aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorTakoroka <137927095+Takoroka@users.noreply.github.com>2026-10-28 20:43:01 -0400
committerTakoroka <137927095+Takoroka@users.noreply.github.com>2026-10-28 20:43:01 -0400
commit0d5534a2883329183ae8aa89b3fe1c7b67291ece (patch)
treeaabad4260f301a8a52af16e15e7fb76a700d642f /src
parente4deef6ba4ea76aac997ed3102f8e9801fd170b9 (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.txt1
-rw-r--r--src/video_core/host_shaders/bc_encoder.comp183
-rw-r--r--src/video_core/renderer_vulkan/vk_compute_pass.cpp129
-rw-r--r--src/video_core/renderer_vulkan/vk_compute_pass.h34
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.cpp123
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.h17
-rw-r--r--src/video_core/texture_cache/image_base.h1
-rw-r--r--src/video_core/texture_cache/texture_cache.h61
-rw-r--r--src/video_core/texture_cache/texture_cache_base.h7
-rw-r--r--src/video_core/texture_cache/util.cpp29
-rw-r--r--src/video_core/texture_cache/util.h3
-rw-r--r--src/video_core/textures/workers.cpp9
-rw-r--r--src/video_core/vulkan_common/vulkan_device.cpp14
-rw-r--r--src/video_core/vulkan_common/vulkan_device.h3
-rw-r--r--src/video_core/vulkan_common/vulkan_memory_allocator.cpp6
-rw-r--r--src/video_core/vulkan_common/vulkan_memory_allocator.h4
-rw-r--r--src/yuzu_cmd/emu_window/emu_window_sdl3.h5
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};
}