diff options
| author | Takoroka <137927095+Takoroka@users.noreply.github.com> | 2026-10-28 20:43:01 -0400 |
|---|---|---|
| committer | Takoroka <137927095+Takoroka@users.noreply.github.com> | 2026-10-28 20:43:01 -0400 |
| commit | 0d5534a2883329183ae8aa89b3fe1c7b67291ece (patch) | |
| tree | aabad4260f301a8a52af16e15e7fb76a700d642f /src | |
| parent | e4deef6ba4ea76aac997ed3102f8e9801fd170b9 (diff) | |
ASTC: decode and re-compress to BC1/BC3 on the GPU; texture cache budget
- accelerate_astc=Gpu with astc_recompression=Bc1/Bc3 now decodes ASTC
with the GPU decoder into a temporary RGBA8 image (dedicated memory,
freed once the GPU is past it) and compresses it with a new compute
shader (bc_encoder.comp: bounding-box endpoints along the colours'
covariance, inset 1/16, nearest palette entry; BC3 alpha ramp; BC1
punch-through), then copies the blocks into the image. No CPU decode,
so streamed textures are ready the frame they are first used instead
of showing placeholders for seconds; the decode pass no longer waits
for the GPU on this path.
- The CPU path (CpuAsynchronous) fills a new image with zero blocks
before its first decode instead of showing stale VRAM, and the
transcode pool is a quarter of the hardware threads.
- Switch 2 titles: collector thresholds near the device budget, re-read
every 60 frames from VK_EXT_memory_budget (another game shrank it from
7.0 to 5.4 GB).
- EDEN_NX2_HIDE_WINDOW=0 now leaves the window visible.
Diffstat (limited to 'src')
| -rw-r--r-- | src/video_core/host_shaders/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | src/video_core/host_shaders/bc_encoder.comp | 183 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/vk_compute_pass.cpp | 129 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/vk_compute_pass.h | 34 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/vk_texture_cache.cpp | 123 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/vk_texture_cache.h | 17 | ||||
| -rw-r--r-- | src/video_core/texture_cache/image_base.h | 1 | ||||
| -rw-r--r-- | src/video_core/texture_cache/texture_cache.h | 61 | ||||
| -rw-r--r-- | src/video_core/texture_cache/texture_cache_base.h | 7 | ||||
| -rw-r--r-- | src/video_core/texture_cache/util.cpp | 29 | ||||
| -rw-r--r-- | src/video_core/texture_cache/util.h | 3 | ||||
| -rw-r--r-- | src/video_core/textures/workers.cpp | 9 | ||||
| -rw-r--r-- | src/video_core/vulkan_common/vulkan_device.cpp | 14 | ||||
| -rw-r--r-- | src/video_core/vulkan_common/vulkan_device.h | 3 | ||||
| -rw-r--r-- | src/video_core/vulkan_common/vulkan_memory_allocator.cpp | 6 | ||||
| -rw-r--r-- | src/video_core/vulkan_common/vulkan_memory_allocator.h | 4 | ||||
| -rw-r--r-- | src/yuzu_cmd/emu_window/emu_window_sdl3.h | 5 |
17 files changed, 596 insertions, 33 deletions
diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt index c4e7ac3f6f..8082e830d6 100644 --- a/src/video_core/host_shaders/CMakeLists.txt +++ b/src/video_core/host_shaders/CMakeLists.txt @@ -15,6 +15,7 @@ set(GLSL_INCLUDES set(SHADER_FILES ${CMAKE_CURRENT_SOURCE_DIR}/astc_decoder.comp + ${CMAKE_CURRENT_SOURCE_DIR}/bc_encoder.comp ${CMAKE_CURRENT_SOURCE_DIR}/blit_color_float.frag ${CMAKE_CURRENT_SOURCE_DIR}/block_linear_unswizzle_2d.comp ${CMAKE_CURRENT_SOURCE_DIR}/blit_color_msaa.frag diff --git a/src/video_core/host_shaders/bc_encoder.comp b/src/video_core/host_shaders/bc_encoder.comp new file mode 100644 index 0000000000..3a87e92dcb --- /dev/null +++ b/src/video_core/host_shaders/bc_encoder.comp @@ -0,0 +1,183 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// Compresses one level of an RGBA8 image (the ASTC decoder's output) to BC1 or BC3, one 4x4 block +// per invocation, so re-compressed ASTC never round-trips through the CPU. Colour endpoints are +// the block's bounding box, oriented along the colours' covariance and inset by 1/16; each texel +// takes the nearest palette entry. Values are encoded as stored (sRGB data stays sRGB, like the +// CPU path). + +#version 460 core + +layout (local_size_x = 8, local_size_y = 8, local_size_z = 1) in; + +layout (set = 0, binding = 0, rgba8) uniform readonly image2DArray source; + +layout (std430, set = 0, binding = 1) writeonly buffer Output { + uvec2 blocks[]; +}; + +layout (push_constant) uniform PushConstants { + uint blocks_x; + uint blocks_y; + uint width; + uint height; + uint bc3; + uint out_offset; // in 8-byte units +}; + +uint Pack565(vec3 color) { + const uvec3 q = uvec3(round(clamp(color, 0.0, 1.0) * vec3(31.0, 63.0, 31.0))); + return (q.r << 11) | (q.g << 5) | q.b; +} + +vec3 Unpack565(uint value) { + const uvec3 q = uvec3((value >> 11) & 31u, (value >> 5) & 63u, value & 31u); + const uvec3 e = uvec3((q.r << 3) | (q.r >> 2), (q.g << 2) | (q.g >> 4), (q.b << 3) | (q.b >> 2)); + return vec3(e) / 255.0; +} + +float Distance2(vec3 a, vec3 b) { + const vec3 d = a - b; + return dot(d, d); +} + +// BC1 colour block. With punch_through (BC1 only), texels with alpha < 0.5 use the transparent +// index of the three-colour mode; otherwise the four-colour mode is used (BC3 always decodes so). +uvec2 EncodeColor(in vec4 texels[16], bool punch_through) { + vec3 lo = vec3(1.0); + vec3 hi = vec3(0.0); + for (int i = 0; i < 16; ++i) { + if (punch_through && texels[i].a < 0.5) { + continue; + } + lo = min(lo, texels[i].rgb); + hi = max(hi, texels[i].rgb); + } + if (any(greaterThan(lo, hi))) { + // Every texel transparent: colour0 == colour1 selects three-colour mode, index 3. + return uvec2(0u, 0xFFFFFFFFu); + } + const vec3 center = (lo + hi) * 0.5; + const vec3 extent = hi - lo; + const int major = extent.g >= extent.r && extent.g >= extent.b ? 1 : (extent.r >= extent.b ? 0 : 2); + vec3 covariance = vec3(0.0); + for (int i = 0; i < 16; ++i) { + if (punch_through && texels[i].a < 0.5) { + continue; + } + const vec3 d = texels[i].rgb - center; + covariance += d * d[major]; + } + vec3 e0 = hi; + vec3 e1 = lo; + for (int c = 0; c < 3; ++c) { + if (covariance[c] < 0.0) { + e0[c] = lo[c]; + e1[c] = hi[c]; + } + } + const vec3 inset = (e0 - e1) / 16.0; + e0 -= inset; + e1 += inset; + + uint q0 = Pack565(e0); + uint q1 = Pack565(e1); + // Four-colour mode needs colour0 > colour1, three-colour mode colour0 <= colour1. + if (punch_through ? q0 > q1 : q0 < q1) { + const uint t = q0; + q0 = q1; + q1 = t; + } + if (!punch_through && q0 == q1) { + return uvec2(q0 | (q1 << 16), 0u); + } + vec3 palette[4]; + palette[0] = Unpack565(q0); + palette[1] = Unpack565(q1); + if (punch_through) { + palette[2] = (palette[0] + palette[1]) * 0.5; + palette[3] = vec3(0.0); + } else { + palette[2] = (2.0 * palette[0] + palette[1]) / 3.0; + palette[3] = (palette[0] + 2.0 * palette[1]) / 3.0; + } + const int entries = punch_through ? 3 : 4; + uint indices = 0u; + for (int i = 0; i < 16; ++i) { + uint best = 0u; + if (punch_through && texels[i].a < 0.5) { + best = 3u; + } else { + float best_distance = Distance2(texels[i].rgb, palette[0]); + for (int p = 1; p < entries; ++p) { + const float distance = Distance2(texels[i].rgb, palette[p]); + if (distance < best_distance) { + best_distance = distance; + best = uint(p); + } + } + } + indices |= best << (2 * i); + } + return uvec2(q0 | (q1 << 16), indices); +} + +// BC3 alpha block: eight-value mode (alpha0 > alpha1), indices 0 = alpha0, 1 = alpha1, 2..7 the +// ramp from alpha0 to alpha1. +uvec2 EncodeAlpha(in vec4 texels[16]) { + float lo = 1.0; + float hi = 0.0; + for (int i = 0; i < 16; ++i) { + lo = min(lo, texels[i].a); + hi = max(hi, texels[i].a); + } + const uint a0 = uint(round(hi * 255.0)); + const uint a1 = uint(round(lo * 255.0)); + uint bits_lo = a0 | (a1 << 8); + uint bits_hi = 0u; + if (a0 == a1) { + return uvec2(bits_lo, bits_hi); + } + for (uint i = 0u; i < 16u; ++i) { + const float t = (texels[i].a * 255.0 - float(a1)) / float(a0 - a1); + const uint q = uint(clamp(round(t * 7.0), 0.0, 7.0)); + const uint index = q == 7u ? 0u : (q == 0u ? 1u : 8u - q); + const uint bit = 16u + 3u * i; + if (bit < 32u) { + bits_lo |= index << bit; + if (bit > 29u) { + bits_hi |= index >> (32u - bit); + } + } else { + bits_hi |= index << (bit - 32u); + } + } + return uvec2(bits_lo, bits_hi); +} + +void main() { + const uvec3 id = gl_GlobalInvocationID; + if (id.x >= blocks_x || id.y >= blocks_y) { + return; + } + vec4 texels[16]; + const ivec2 base = ivec2(id.xy) * 4; + const ivec2 last = ivec2(width, height) - 1; + bool any_transparent = false; + for (int y = 0; y < 4; ++y) { + for (int x = 0; x < 4; ++x) { + // Edge blocks repeat the last row/column so padding doesn't widen the endpoints. + const ivec2 coord = min(base + ivec2(x, y), last); + texels[y * 4 + x] = imageLoad(source, ivec3(coord, int(id.z))); + any_transparent = any_transparent || texels[y * 4 + x].a < 0.5; + } + } + const uint block = (id.z * blocks_y + id.y) * blocks_x + id.x; + if (bc3 != 0u) { + blocks[out_offset + block * 2u] = EncodeAlpha(texels); + blocks[out_offset + block * 2u + 1u] = EncodeColor(texels, false); + } else { + blocks[out_offset + block] = EncodeColor(texels, any_transparent); + } +} diff --git a/src/video_core/renderer_vulkan/vk_compute_pass.cpp b/src/video_core/renderer_vulkan/vk_compute_pass.cpp index ba521520a0..b3ebf7b3f6 100644 --- a/src/video_core/renderer_vulkan/vk_compute_pass.cpp +++ b/src/video_core/renderer_vulkan/vk_compute_pass.cpp @@ -20,6 +20,7 @@ #include "common/div_ceil.h" #include "common/vector_math.h" #include "video_core/host_shaders/astc_decoder_comp_spv.h" +#include "video_core/host_shaders/bc_encoder_comp_spv.h" #include "video_core/host_shaders/queries_prefix_scan_sum_comp_spv.h" #include "video_core/host_shaders/queries_prefix_scan_sum_nosubgroups_comp_spv.h" #include "video_core/host_shaders/resolve_conditional_render_comp_spv.h" @@ -190,6 +191,51 @@ struct AstcPushConstants { u32 native_tiling; }; +constexpr std::array<VkDescriptorSetLayoutBinding, 2> BC_ENCODER_DESCRIPTOR_SET_BINDINGS{{ + { + .binding = 0, + .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, + .descriptorCount = 1, + .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, + .pImmutableSamplers = nullptr, + }, + { + .binding = 1, + .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, + .descriptorCount = 1, + .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, + .pImmutableSamplers = nullptr, + }, +}}; + +constexpr std::array<VkDescriptorUpdateTemplateEntry, 2> BC_ENCODER_DESCRIPTOR_UPDATE_TEMPLATE{{ + { + .dstBinding = 0, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, + .offset = 0, + .stride = sizeof(DescriptorUpdateEntry), + }, + { + .dstBinding = 1, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, + .offset = sizeof(DescriptorUpdateEntry), + .stride = sizeof(DescriptorUpdateEntry), + }, +}}; + +struct BcEncoderPushConstants { + u32 blocks_x; + u32 blocks_y; + u32 width; + u32 height; + u32 bc3; + u32 out_offset; // in 8-byte units +}; + struct QueriesPrefixScanPushConstants { u32 min_accumulation_base; u32 max_accumulation_base; @@ -526,16 +572,31 @@ ASTCDecoderPass::~ASTCDecoderPass() = default; void ASTCDecoderPass::Assemble(Image& image, const StagingBufferRef& map, std::span<const VideoCommon::SwizzleParameters> swizzles) { + const Target target{ + .image = image.Handle(), + .aspect_mask = image.AspectMask(), + .initialized = image.ExchangeInitialization(), + .level_view = [&image](s32 host_level) { return image.StorageImageView(host_level); }, + .wait = true, + }; + AssembleInto(image.info, image.host_level_offset, image.guest_size_bytes, target, map, + swizzles); +} + +void ASTCDecoderPass::AssembleInto(const VideoCommon::ImageInfo& info, u32 skip, + size_t guest_size_bytes, const Target& target, + const StagingBufferRef& map, + std::span<const VideoCommon::SwizzleParameters> swizzles) { using namespace VideoCommon::Accelerated; const std::array<u32, 2> block_dims{ - VideoCore::Surface::DefaultBlockWidth(image.info.format), - VideoCore::Surface::DefaultBlockHeight(image.info.format), + VideoCore::Surface::DefaultBlockWidth(info.format), + VideoCore::Surface::DefaultBlockHeight(info.format), }; scheduler.RequestOutsideRenderPassOperationContext(); const VkPipeline vk_pipeline = *pipeline; - const VkImageAspectFlags aspect_mask = image.AspectMask(); - const VkImage vk_image = image.Handle(); - const bool is_initialized = image.ExchangeInitialization(); + const VkImageAspectFlags aspect_mask = target.aspect_mask; + const VkImage vk_image = target.image; + const bool is_initialized = target.initialized; scheduler.Record([vk_pipeline, vk_image, aspect_mask, is_initialized](vk::CommandBuffer cmdbuf) { const VkImageMemoryBarrier image_barrier{ @@ -564,28 +625,27 @@ void ASTCDecoderPass::Assemble(Image& image, const StagingBufferRef& map, }); // With trimmed host levels the input holds each layer's mip chain from host_level_offset on // (see TextureCache::UploadImageContents), so its layer stride is that tail's size. - const u32 skip = image.host_level_offset; const u32 input_layer_stride = - skip != 0 ? image.info.layer_stride - - static_cast<u32>(VideoCommon::FullUploadSwizzles(image.info)[skip].buffer_offset) - : image.info.layer_stride; - const size_t input_size = skip != 0 ? size_t{input_layer_stride} * image.info.resources.layers - : image.guest_size_bytes; + skip != 0 ? info.layer_stride - + static_cast<u32>(VideoCommon::FullUploadSwizzles(info)[skip].buffer_offset) + : info.layer_stride; + const size_t input_size = skip != 0 ? size_t{input_layer_stride} * info.resources.layers + : guest_size_bytes; for (const VideoCommon::SwizzleParameters& swizzle : swizzles) { const size_t input_offset = swizzle.buffer_offset + map.offset; const u32 num_dispatches_x = Common::DivCeil(swizzle.num_tiles.width, 8U); const u32 num_dispatches_y = Common::DivCeil(swizzle.num_tiles.height, 8U); - const u32 num_dispatches_z = image.info.resources.layers; + const u32 num_dispatches_z = info.resources.layers; compute_pass_descriptor_queue.Acquire(scheduler, 2); compute_pass_descriptor_queue.AddBuffer(map.buffer, input_offset, input_size - swizzle.buffer_offset); compute_pass_descriptor_queue.AddImage( - image.StorageImageView(swizzle.level - static_cast<s32>(skip))); + target.level_view(swizzle.level - static_cast<s32>(skip))); const void* const descriptor_data{compute_pass_descriptor_queue.UpdateData()}; // To unswizzle the ASTC data - auto params = MakeBlockLinearSwizzle2DParams(swizzle, image.info); + auto params = MakeBlockLinearSwizzle2DParams(swizzle, info); params.layer_stride = input_layer_stride; ASSERT(params.origin == (std::array<u32, 3>{0, 0, 0})); ASSERT(params.destination == (std::array<s32, 3>{0, 0, 0})); @@ -636,6 +696,9 @@ void ASTCDecoderPass::Assemble(Image& image, const StagingBufferRef& map, const char* value = std::getenv("EDEN_NX2_ASTC_FLUSH"); return value != nullptr && value[0] == '1'; }(); + if (!target.wait) { + return; + } const auto start = std::chrono::steady_clock::now(); if (flush_only) { scheduler.Flush(); @@ -659,6 +722,44 @@ void ASTCDecoderPass::Assemble(Image& image, const StagingBufferRef& map, } } +BcEncoderPass::BcEncoderPass(const Device& device_, Scheduler& scheduler_, + DescriptorPool& descriptor_pool_, + ComputePassDescriptorQueue& compute_pass_descriptor_queue_) + : ComputePass(device_, scheduler_, descriptor_pool_, BC_ENCODER_DESCRIPTOR_SET_BINDINGS, + BC_ENCODER_DESCRIPTOR_UPDATE_TEMPLATE, ASTC_BANK_INFO, + COMPUTE_PUSH_CONSTANT_RANGE<sizeof(BcEncoderPushConstants)>, BC_ENCODER_COMP_SPV), + scheduler{scheduler_}, compute_pass_descriptor_queue{compute_pass_descriptor_queue_} {} + +BcEncoderPass::~BcEncoderPass() = default; + +void BcEncoderPass::Encode(VkImageView source, u32 width, u32 height, u32 layers, bool bc3, + VkBuffer buffer, VkDeviceSize buffer_offset, VkDeviceSize buffer_size, + VkDeviceSize out_offset) { + scheduler.RequestOutsideRenderPassOperationContext(); + compute_pass_descriptor_queue.Acquire(scheduler, 2); + compute_pass_descriptor_queue.AddImage(source); + compute_pass_descriptor_queue.AddBuffer(buffer, buffer_offset, buffer_size); + const void* const descriptor_data{compute_pass_descriptor_queue.UpdateData()}; + const BcEncoderPushConstants uniforms{ + .blocks_x = Common::DivCeil(width, 4U), + .blocks_y = Common::DivCeil(height, 4U), + .width = width, + .height = height, + .bc3 = bc3 ? 1U : 0U, + .out_offset = static_cast<u32>(out_offset / 8), + }; + const VkPipeline vk_pipeline = *pipeline; + scheduler.Record([this, vk_pipeline, uniforms, layers, descriptor_data](vk::CommandBuffer cmdbuf) { + const VkDescriptorSet set = descriptor_allocator.Commit(); + device.GetLogical().UpdateDescriptorSet(set, *descriptor_template, descriptor_data); + cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, vk_pipeline); + cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *layout, 0, set, {}); + cmdbuf.PushConstants(*layout, VK_SHADER_STAGE_COMPUTE_BIT, uniforms); + cmdbuf.Dispatch(Common::DivCeil(uniforms.blocks_x, 8U), Common::DivCeil(uniforms.blocks_y, 8U), + layers); + }); +} + constexpr u32 BL3D_BINDING_INPUT_BUFFER = 0; constexpr u32 BL3D_BINDING_OUTPUT_BUFFER = 1; diff --git a/src/video_core/renderer_vulkan/vk_compute_pass.h b/src/video_core/renderer_vulkan/vk_compute_pass.h index 3d30aa6bdc..5daf4eb3de 100644 --- a/src/video_core/renderer_vulkan/vk_compute_pass.h +++ b/src/video_core/renderer_vulkan/vk_compute_pass.h @@ -6,6 +6,7 @@ #pragma once +#include <functional> #include <optional> #include <span> #include <utility> @@ -130,6 +131,21 @@ public: void Assemble(Image& image, const StagingBufferRef& map, std::span<const VideoCommon::SwizzleParameters> swizzles); + /// Where a decode writes: an RGBA8 image with a storage view per (host) level. + struct Target { + VkImage image; + VkImageAspectFlags aspect_mask; + bool initialized; + std::function<VkImageView(s32 host_level)> level_view; + bool wait; ///< Wait for the GPU afterwards (the decoded image itself is sampled). + }; + + /// Decodes `info`'s ASTC data in `map` into `target`. `skip` leading levels are absent on the + /// host (TrimmedLevels) and `guest_size_bytes` is the full guest size. + void AssembleInto(const VideoCommon::ImageInfo& info, u32 skip, size_t guest_size_bytes, + const Target& target, const StagingBufferRef& map, + std::span<const VideoCommon::SwizzleParameters> swizzles); + private: Scheduler& scheduler; StagingBufferPool& staging_buffer_pool; @@ -137,6 +153,24 @@ private: MemoryAllocator& memory_allocator; }; +/// Compresses an RGBA8 image level to BC1 or BC3 blocks in a buffer (bc_encoder.comp). +class BcEncoderPass final : public ComputePass { +public: + explicit BcEncoderPass(const Device& device_, Scheduler& scheduler_, + DescriptorPool& descriptor_pool_, + ComputePassDescriptorQueue& compute_pass_descriptor_queue_); + ~BcEncoderPass(); + + /// Encodes every layer of one level (`source` is its RGBA8 storage view) into `buffer`, + /// `out_offset` bytes past `buffer_offset`; blocks are row-major, layer after layer. + void Encode(VkImageView source, u32 width, u32 height, u32 layers, bool bc3, VkBuffer buffer, + VkDeviceSize buffer_offset, VkDeviceSize buffer_size, VkDeviceSize out_offset); + +private: + Scheduler& scheduler; + ComputePassDescriptorQueue& compute_pass_descriptor_queue; +}; + class BlockLinearUnswizzle3DPass final : public ComputePass { public: explicit BlockLinearUnswizzle3DPass(const Device& device_, Scheduler& scheduler_, diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp index cfc6483a04..f5fcdd1380 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp @@ -193,6 +193,17 @@ constexpr std::array<VkFormat, 2> OpaqueAstcViewFormats{VK_FORMAT_BC1_RGBA_UNORM return HugeAstcHostFormat(device, info); } +// ASTC that the GPU decodes and re-compresses to BC1/BC3 itself (TranscodeAstcToBcn): no CPU +// decode, so streamed textures are ready the frame they are first used instead of seconds later. +[[nodiscard]] bool WillUseGpuBcnTranscode(const Device& device, const ImageInfo& info) { + if (!IsPixelFormatASTC(info.format) || device.IsOptimalAstcSupported() || + IsHugeAstcArray(device, info) || info.type != ImageType::e2D || info.size.depth != 1) { + return false; + } + return Settings::values.accelerate_astc.GetValue() == Settings::AstcDecodeMode::Gpu && + Settings::values.astc_recompression.GetValue() != Settings::AstcRecompression::Uncompressed; +} + [[nodiscard]] bool WillUseAcceleratedAstcDecode(const Device& device, const ImageInfo& info) { if (!IsPixelFormatASTC(info.format) || device.IsOptimalAstcSupported()) { return false; @@ -1043,6 +1054,11 @@ TextureCacheRuntime::TextureCacheRuntime(const Device& device_, Scheduler& sched !device.IsOptimalAstcSupported()) { astc_decoder_pass.emplace(device, scheduler, descriptor_pool, staging_buffer_pool, compute_pass_descriptor_queue, memory_allocator); + if (Settings::values.astc_recompression.GetValue() != + Settings::AstcRecompression::Uncompressed) { + bc_encoder_pass.emplace(device, scheduler, descriptor_pool, + compute_pass_descriptor_queue); + } } if (!device.IsKhrImageFormatListSupported()) { return; @@ -1894,6 +1910,10 @@ u64 TextureCacheRuntime::GetDeviceMemoryUsage() const { return device.GetDeviceMemoryUsage(); } +u64 TextureCacheRuntime::GetDeviceMemoryBudget() const { + return device.GetDeviceMemoryBudget(); +} + bool TextureCacheRuntime::CanDownloadMsaa(const VideoCommon::ImageInfo& info) const { const VkImageAspectFlags aspect_mask = ImageAspectMask(info.format); if ((aspect_mask & VK_IMAGE_ASPECT_COLOR_BIT) != 0) { @@ -1971,6 +1991,9 @@ void TextureCacheRuntime::ReleaseMsaaScratchImage(VkImage image) { } void TextureCacheRuntime::TickFrame() { + while (!pending_transcodes.empty() && scheduler.IsFree(pending_transcodes.front().tick)) { + pending_transcodes.pop_front(); + } static constexpr u32 MAX_UNUSED_SCRATCH_FRAMES = 60; std::erase_if(msaa_scratch_images, [this](MsaaScratchImage& scratch) { if (!scheduler.IsFree(scratch.tick)) { @@ -2010,7 +2033,8 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu } else if (IsPixelFormatASTC(info.format) && !runtime->device.IsOptimalAstcSupported()) { switch (Settings::values.accelerate_astc.GetValue()) { case Settings::AstcDecodeMode::Gpu: - if (WillUseAcceleratedAstcDecode(runtime->device, info)) { + if (WillUseAcceleratedAstcDecode(runtime->device, info) || + WillUseGpuBcnTranscode(runtime->device, info)) { flags |= VideoCommon::ImageFlagBits::AcceleratedUpload; } break; @@ -3281,6 +3305,9 @@ void TextureCacheRuntime::AccelerateImageUpload( u32 z_start, u32 z_count) { if (IsPixelFormatASTC(image.info.format)) { + if (bc_encoder_pass && WillUseGpuBcnTranscode(device, image.info)) { + return TranscodeAstcToBcn(image, map, swizzles); + } return astc_decoder_pass->Assemble(image, map, swizzles); } @@ -3299,6 +3326,100 @@ void TextureCacheRuntime::AccelerateImageUpload( ASSERT(false); } +void TextureCacheRuntime::TranscodeAstcToBcn( + Image& image, const StagingBufferRef& map, + std::span<const VideoCommon::SwizzleParameters> swizzles) { + const ImageInfo& info = image.info; + const u32 width = info.size.width; + const u32 height = info.size.height; + const u32 levels = static_cast<u32>(info.resources.levels); + const u32 layers = static_cast<u32>(info.resources.layers); + constexpr VkFormat DecodeFormat = VK_FORMAT_A8B8G8R8_UNORM_PACK32; + PendingTranscode pending{ + .tick = 0, + .image = memory_allocator.CreateImage(VkImageCreateInfo{ + .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .imageType = VK_IMAGE_TYPE_2D, + .format = DecodeFormat, + .extent = {width, height, 1}, + .mipLevels = levels, + .arrayLayers = layers, + .samples = VK_SAMPLE_COUNT_1_BIT, + .tiling = VK_IMAGE_TILING_OPTIMAL, + .usage = VK_IMAGE_USAGE_STORAGE_BIT, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = 0, + .pQueueFamilyIndices = nullptr, + .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, + }, true), + .views = {}, + }; + for (u32 level = 0; level < levels; ++level) { + pending.views.push_back( + MakeStorageView(device.GetLogical(), level, *pending.image, DecodeFormat)); + } + astc_decoder_pass->AssembleInto( + info, 0, image.guest_size_bytes, + ASTCDecoderPass::Target{ + .image = *pending.image, + .aspect_mask = VK_IMAGE_ASPECT_COLOR_BIT, + .initialized = false, + .level_view = [&pending](s32 level) { return *pending.views[level]; }, + .wait = false, + }, + map, swizzles); + + // Encode every level into one buffer, then copy it into the image. + const bool bc3 = !IsOpaqueBc1Astc(device, info) && + Settings::values.astc_recompression.GetValue() != + Settings::AstcRecompression::Bc1; + const u64 block_bytes = bc3 ? 16 : 8; + boost::container::small_vector<VideoCommon::BufferImageCopy, 16> copies; + u64 total = 0; + for (u32 level = 0; level < levels; ++level) { + const u32 level_width = (std::max)(width >> level, 1U); + const u32 level_height = (std::max)(height >> level, 1U); + const u32 blocks_x = Common::DivCeil(level_width, 4U); + const u32 blocks_y = Common::DivCeil(level_height, 4U); + const u64 size = u64{blocks_x} * blocks_y * layers * block_bytes; + copies.push_back({ + .buffer_offset = total, + .buffer_size = size, + .buffer_row_length = blocks_x * 4, + .buffer_image_height = blocks_y * 4, + .image_subresource{ + .base_level = static_cast<s32>(level), + .base_layer = 0, + .num_layers = static_cast<s32>(layers), + }, + .image_offset{0, 0, 0}, + .image_extent{level_width, level_height, 1}, + }); + total += size; + } + const auto output = staging_buffer_pool.Request(total, MemoryUsage::DeviceLocal); + for (u32 level = 0; level < levels; ++level) { + bc_encoder_pass->Encode(*pending.views[level], copies[level].image_extent.width, + copies[level].image_extent.height, layers, bc3, output.buffer, + output.offset, total, copies[level].buffer_offset); + } + scheduler.Record([](vk::CommandBuffer cmdbuf) { + const VkMemoryBarrier barrier{ + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT, + .dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT, + }; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, barrier); + }); + image.UploadMemory(output.buffer, output.offset, copies); + pending.tick = scheduler.CurrentTick(); + pending_transcodes.push_back(std::move(pending)); +} + void TextureCacheRuntime::TransitionImageLayout(Image& image) { if (!image.ExchangeInitialization()) { VkImageMemoryBarrier barrier{ diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.h b/src/video_core/renderer_vulkan/vk_texture_cache.h index 1d171b576f..1d92d439f4 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.h +++ b/src/video_core/renderer_vulkan/vk_texture_cache.h @@ -6,6 +6,7 @@ #pragma once +#include <deque> #include <span> #include "video_core/texture_cache/texture_cache_base.h" @@ -66,6 +67,8 @@ public: u64 GetDeviceMemoryUsage() const; + u64 GetDeviceMemoryBudget() const; + bool CanReportMemoryUsage() const; void LogMemoryStatistics() const; @@ -162,6 +165,20 @@ public: BlitImageHelper& blit_image_helper; RenderPassCache& render_pass_cache; std::optional<ASTCDecoderPass> astc_decoder_pass; + std::optional<BcEncoderPass> bc_encoder_pass; + + /// ASTC decoded on the GPU and re-compressed to BC1/BC3 there (accelerate_astc=Gpu with + /// astc_recompression=Bc1/Bc3): decode into a temporary RGBA8 image, encode, copy. + void TranscodeAstcToBcn(Image& image, const StagingBufferRef& map, + std::span<const VideoCommon::SwizzleParameters> swizzles); + + /// Temporary decode images, freed once the GPU is past `tick`. + struct PendingTranscode { + u64 tick; + vk::Image image; + std::vector<vk::ImageView> views; + }; + std::deque<PendingTranscode> pending_transcodes; std::optional<BlockLinearUnswizzle3DPass> bl3d_unswizzle_pass; const Settings::ResolutionScalingInfo& resolution; diff --git a/src/video_core/texture_cache/image_base.h b/src/video_core/texture_cache/image_base.h index d0af53631d..47bab9617a 100644 --- a/src/video_core/texture_cache/image_base.h +++ b/src/video_core/texture_cache/image_base.h @@ -49,6 +49,7 @@ enum class ImageFlagBits : u32 { /// The host image keeps only mip levels >= host_level_offset (its decoded copy would not fit /// in VRAM); uploads skip the rest, downloads are never done. TrimmedLevels = 1 << 19, + ZeroFilled = 1 << 20, ///< Cleared to zero blocks before its first asynchronous decode. }; DECLARE_ENUM_FLAG_OPERATORS(ImageFlagBits) diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index fbcd3eefbb..75df31dc27 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -80,15 +80,7 @@ TextureCache<P>::TextureCache(Runtime& runtime_, Tegra::MaxwellDeviceMemoryManag critical_memory = DEFAULT_CRITICAL_MEMORY + 1_GiB; minimum_memory = 0; } - // EDEN_NX2_TC_EXPECTED_MIB / EDEN_NX2_TC_CRITICAL_MIB override the collector's thresholds. - for (const auto& [name, target] : {std::pair{"EDEN_NX2_TC_EXPECTED_MIB", &expected_memory}, - std::pair{"EDEN_NX2_TC_CRITICAL_MIB", &critical_memory}}) { - if (const char* value = std::getenv(name)) { - *target = std::strtoull(value, nullptr, 10) << 20; - } - } - LOG_INFO(HW_GPU, "Texture cache thresholds: expected {} MiB, critical {} MiB, minimum {} MiB", - expected_memory >> 20, critical_memory >> 20, minimum_memory >> 20); + ApplyThresholdOverrides(); const bool gpu_unswizzle_enabled = Settings::values.gpu_unswizzle_enabled.GetValue(); @@ -182,7 +174,46 @@ void TextureCache<P>::RunGarbageCollector() { } template <class P> +void TextureCache<P>::ApplyThresholdOverrides() { + // EDEN_NX2_TC_EXPECTED_MIB / EDEN_NX2_TC_CRITICAL_MIB override the collector's thresholds. + for (const auto& [name, target] : {std::pair{"EDEN_NX2_TC_EXPECTED_MIB", &expected_memory}, + std::pair{"EDEN_NX2_TC_CRITICAL_MIB", &critical_memory}}) { + if (const char* value = std::getenv(name)) { + *target = std::strtoull(value, nullptr, 10) << 20; + } + } + LOG_INFO(HW_GPU, "Texture cache thresholds: expected {} MiB, critical {} MiB, minimum {} MiB", + expected_memory >> 20, critical_memory >> 20, minimum_memory >> 20); +} + +template <class P> void TextureCache<P>::TickFrame() { + if constexpr (HAS_DEVICE_MEMORY_INFO) { + // Switch 2 titles (native tiling) stream far more texture data than Eden's thresholds + // were tuned for: title 1's attract loop alone keeps ~5.3 GB resident, so the collector + // sat in aggressive mode evicting decoded ASTC (and decoding it again). Total use is + // measured from the driver, so collect only near the device budget instead. + if (!native_thresholds_applied && gpu_memory != nullptr && gpu_memory->UsesNativeTiling()) { + native_thresholds_applied = true; + const u64 device_local = runtime.GetDeviceLocalMemory(); + critical_memory = (std::max)(critical_memory, device_local - 256_MiB); + expected_memory = (std::max)(expected_memory, device_local - 1280_MiB); + ApplyThresholdOverrides(); + native_critical_memory = critical_memory; + native_expected_memory = expected_memory; + } + // The budget shrinks when other programs use VRAM (another game: 7.0 -> 6.4 GB), and + // collecting only past it means the driver pages textures out. Re-read it now and then. + if constexpr (requires { runtime.GetDeviceMemoryBudget(); }) { + if (native_thresholds_applied && ++budget_check_frames % 60 == 0) { + const u64 budget = runtime.GetDeviceMemoryBudget(); + if (budget > 2_GiB) { + critical_memory = (std::min)(native_critical_memory, budget - 512_MiB); + expected_memory = (std::min)(native_expected_memory, budget - 1280_MiB); + } + } + } + } // If we can obtain the memory info, use it instead of the estimate. if (runtime.CanReportMemoryUsage()) { total_used_memory = runtime.GetDeviceMemoryUsage(); @@ -1492,6 +1523,18 @@ void TextureCache<P>::QueueAsyncDecode(Image& image, ImageId image_id) { Tegra::Memory::GpuGuestMemory<u8, Tegra::Memory::GuestMemoryFlags::UnsafeRead> swizzle_data(*gpu_memory, image.gpu_addr, image.guest_size_bytes, &swizzle_data_buffer); auto copies = UnswizzleImage(*gpu_memory, image.gpu_addr, image.info, swizzle_data, local_unswizzle_data_buffer); const size_t out_size = MapSizeBytes(image); + if (False(image.flags & ImageFlagBits::ZeroFilled)) { + // Until the decode lands (a second or more while title 1 streams ~40 textures a second) + // a new image showed whatever its memory last held, often other textures. Zero blocks + // read as opaque black (BC1) or transparent (BC3) instead. + image.flags |= ImageFlagBits::ZeroFilled; + auto zero_copies = copies; + ConvertedLayout(image.info, zero_copies); + auto staging = runtime.UploadStagingBuffer(out_size); + std::memset(staging.mapped_span.data(), 0, out_size); + image.UploadMemory(staging, zero_copies); + runtime.InsertUploadMemoryBarrier(); + } auto func = [out_size, copies, info = image.info, input = std::move(local_unswizzle_data_buffer), diff --git a/src/video_core/texture_cache/texture_cache_base.h b/src/video_core/texture_cache/texture_cache_base.h index f6f083e10a..ce1de8e867 100644 --- a/src/video_core/texture_cache/texture_cache_base.h +++ b/src/video_core/texture_cache/texture_cache_base.h @@ -126,6 +126,10 @@ class TextureCache : public VideoCommon::ChannelSetupCaches<TextureCacheChannelI u64 gc_costly_deletes{}; u64 async_decodes_queued{}; u64 images_created{}; + bool native_thresholds_applied{}; + u64 native_critical_memory{}; + u64 native_expected_memory{}; + u64 budget_check_frames{}; u64 astc_opaque_images{}; u64 astc_alpha_images{}; std::vector<u8> astc_scan_buffer; @@ -462,6 +466,9 @@ private: DAddr virtual_invalid_space{}; + /// Applies EDEN_NX2_TC_EXPECTED_MIB / EDEN_NX2_TC_CRITICAL_MIB and logs the thresholds. + void ApplyThresholdOverrides(); + /// The CPU address an image at gpu_addr is keyed by: its backing, or a fixed stand-in. [[nodiscard]] std::optional<DAddr> ImageCpuAddress(GPUVAddr gpu_addr, size_t size) const; diff --git a/src/video_core/texture_cache/util.cpp b/src/video_core/texture_cache/util.cpp index 7a1a1d89c0..ffc1d59ec3 100644 --- a/src/video_core/texture_cache/util.cpp +++ b/src/video_core/texture_cache/util.cpp @@ -929,6 +929,35 @@ boost::container::small_vector<BufferImageCopy, 16> UnswizzleImage(Tegra::Memory return copies; } +void ConvertedLayout(const ImageInfo& info, std::span<BufferImageCopy> copies) { + // Same offsets and sizes as ConvertImage below. + const auto recompression = Settings::values.astc_recompression.GetValue(); + const bool astc = IsPixelFormatASTC(info.format); + u64 output_offset = 0; + for (BufferImageCopy& copy : copies) { + const Extent3D mip_size = AdjustMipSize(info.size, copy.image_subresource.base_level); + const u64 layers = static_cast<u64>(copy.image_subresource.num_layers) * copy.image_extent.depth; + u64 size; + if (astc && recompression == Settings::AstcRecompression::Uncompressed) { + size = u64{copy.image_extent.width} * copy.image_extent.height * layers * + BytesPerBlock(PixelFormat::A8B8G8R8_UNORM); + } else if (astc) { + const u64 bpp_div = + recompression == Settings::AstcRecompression::Bc1 || info.astc_opaque ? 2 : 1; + size = u64{Common::AlignUp(copy.image_extent.width, 4U)} * + Common::AlignUp(copy.image_extent.height, 4U) * layers / bpp_div; + } else { + size = u64{copy.image_extent.width} * copy.image_extent.height * layers * + ConvertedBytesPerBlock(info.format); + } + copy.buffer_offset = output_offset; + copy.buffer_size = size; + copy.buffer_row_length = mip_size.width; + copy.buffer_image_height = mip_size.height; + output_offset += size; + } +} + void ConvertImage(std::span<const u8> input, const ImageInfo& info, std::span<u8> output, std::span<BufferImageCopy> copies) { u32 output_offset = 0; diff --git a/src/video_core/texture_cache/util.h b/src/video_core/texture_cache/util.h index 89f85e4e8a..0381d2d24b 100644 --- a/src/video_core/texture_cache/util.h +++ b/src/video_core/texture_cache/util.h @@ -72,6 +72,9 @@ struct OverlapResult { Tegra::MemoryManager& gpu_memory, GPUVAddr gpu_addr, const ImageInfo& info, std::span<const u8> input, std::span<u8> output); +/// Rewrites `copies` (from UnswizzleImage) to the layout ConvertImage produces, without converting. +void ConvertedLayout(const ImageInfo& info, std::span<BufferImageCopy> copies); + void ConvertImage(std::span<const u8> input, const ImageInfo& info, std::span<u8> output, std::span<BufferImageCopy> copies); diff --git a/src/video_core/textures/workers.cpp b/src/video_core/textures/workers.cpp index 6922634e30..e3a0f26cf2 100644 --- a/src/video_core/textures/workers.cpp +++ b/src/video_core/textures/workers.cpp @@ -11,14 +11,15 @@ namespace Tegra::Texture { Common::ThreadWorker& GetThreadWorkers() { - // An eighth of the hardware threads (at least 2): with half, re-compressing streamed ASTC - // textures took ~3.5 cores from the emulated CPU cores and the GPU thread on an 8-core - // laptop. EDEN_NX2_TRANSCODE_THREADS=<n> overrides. + // A quarter of the hardware threads (at least 2). Half took ~3.5 cores from the emulated + // CPU cores and the GPU thread on an 8-core laptop when textures were thrashing; with an + // eighth (2) both threads stayed saturated and streamed textures arrived seconds late (shown + // as placeholders), while 4 cost no frame rate. EDEN_NX2_TRANSCODE_THREADS=<n> overrides. static Common::ThreadWorker workers{[] { if (const char* text = std::getenv("EDEN_NX2_TRANSCODE_THREADS")) { return (std::max)(static_cast<u32>(std::strtoul(text, nullptr, 10)), 1U); } - return (std::max)(std::thread::hardware_concurrency() / 8, 2U); + return (std::max)(std::thread::hardware_concurrency() / 4, 2U); }(), "ImageTranscode"}; return workers; } diff --git a/src/video_core/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp index be09bca8d3..454e5d4e02 100644 --- a/src/video_core/vulkan_common/vulkan_device.cpp +++ b/src/video_core/vulkan_common/vulkan_device.cpp @@ -1612,6 +1612,20 @@ u64 Device::GetDeviceMemoryUsage() const { return result; } +u64 Device::GetDeviceMemoryBudget() const { + if (!extensions.memory_budget) { + return 0; + } + VkPhysicalDeviceMemoryBudgetPropertiesEXT budget{}; + budget.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT; + physical.GetMemoryProperties(&budget); + u64 result{}; + for (const size_t heap : valid_heap_memory) { + result += budget.heapBudget[heap]; + } + return result; +} + void Device::CollectPhysicalMemoryInfo() { // Calculate limits using memory budget VkPhysicalDeviceMemoryBudgetPropertiesEXT budget{}; diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h index bbf3754e39..34ee63a275 100644 --- a/src/video_core/vulkan_common/vulkan_device.h +++ b/src/video_core/vulkan_common/vulkan_device.h @@ -963,6 +963,9 @@ FN_MAX_LIMIT_LIST u64 GetDeviceMemoryUsage() const; + /// The driver's current memory budget for this process (VK_EXT_memory_budget), 0 without it. + u64 GetDeviceMemoryBudget() const; + u32 GetSetsPerPool() const { return sets_per_pool; } diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp index 23f1b0a798..27f8fa6b4c 100644 --- a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp +++ b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp @@ -206,10 +206,12 @@ MemoryAllocator::MemoryAllocator(const Device &device_) MemoryAllocator::~MemoryAllocator() = default; -vk::Image MemoryAllocator::CreateImage(const VkImageCreateInfo &ci) const +vk::Image MemoryAllocator::CreateImage(const VkImageCreateInfo &ci, bool dedicated) const { const VmaAllocationCreateInfo alloc_ci = { - .flags = VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT, + .flags = static_cast<VmaAllocationCreateFlags>(VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT) | + (dedicated ? VmaAllocationCreateFlags{VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT} + : VmaAllocationCreateFlags{0}), .usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE, .requiredFlags = 0, .preferredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.h b/src/video_core/vulkan_common/vulkan_memory_allocator.h index 7467e929d3..fa7d4b41c3 100644 --- a/src/video_core/vulkan_common/vulkan_memory_allocator.h +++ b/src/video_core/vulkan_common/vulkan_memory_allocator.h @@ -103,7 +103,9 @@ namespace Vulkan { MemoryAllocator(const MemoryAllocator &) = delete; - vk::Image CreateImage(const VkImageCreateInfo &ci) const; + /// `dedicated`: own device memory, returned to the driver on destruction (short-lived + /// images would otherwise leave holes in the shared blocks). + vk::Image CreateImage(const VkImageCreateInfo &ci, bool dedicated = false) const; vk::Buffer CreateBuffer(const VkBufferCreateInfo &ci, MemoryUsage usage) const; diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl3.h b/src/yuzu_cmd/emu_window/emu_window_sdl3.h index bcac169849..0dc2a4983a 100644 --- a/src/yuzu_cmd/emu_window/emu_window_sdl3.h +++ b/src/yuzu_cmd/emu_window/emu_window_sdl3.h @@ -25,10 +25,11 @@ class InputSubsystem; enum class MouseButton; } // namespace InputCommon -// EDEN_NX2_HIDE_WINDOW=1 creates the render window hidden and without focus, for unattended +// EDEN_NX2_HIDE_WINDOW=1 (anything but 0) creates the render window hidden and without focus, for unattended // test runs that must not cover or interrupt the desktop. inline SDL_WindowFlags Nx2HiddenWindowFlags() { - return std::getenv("EDEN_NX2_HIDE_WINDOW") != nullptr ? SDL_WINDOW_HIDDEN | SDL_WINDOW_NOT_FOCUSABLE + const char* hide = std::getenv("EDEN_NX2_HIDE_WINDOW"); + return hide != nullptr && hide[0] != '0' ? SDL_WINDOW_HIDDEN | SDL_WINDOW_NOT_FOCUSABLE : SDL_WindowFlags{0}; } |
