From 0ec2b0e9ffd0d89eb0d7b4e9cc7acc7acc2a6386 Mon Sep 17 00:00:00 2001 From: Forrest Keller Date: Sun, 11 Jan 2026 21:02:20 -0600 Subject: [PATCH] Added hack setting to partial fix shadow boxes in MP4 --- .../features/settings/model/BooleanSetting.kt | 1 + .../settings/model/view/SettingsItem.kt | 7 +++ .../settings/ui/SettingsFragmentPresenter.kt | 1 + .../app/src/main/res/values/strings.xml | 3 +- src/common/settings.h | 2 + src/qt_common/config/shared_translation.cpp | 4 ++ .../renderer_vulkan/vk_graphics_pipeline.cpp | 35 +++++++++++-- src/video_core/texture_cache/texture_cache.h | 49 ++++++++++++++++++- 8 files changed, 95 insertions(+), 7 deletions(-) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt index 2418003904..45249c74e6 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt @@ -39,6 +39,7 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting { USE_CUSTOM_RTC("custom_rtc_enabled"), BLACK_BACKGROUNDS("black_backgrounds"), INVERT_CONFIRM_BACK_CONTROLLER_BUTTONS("invert_confirm_back_controller_buttons"), + HACK_FIX_SHADOWARRAY("fix_shadow_array_handles"), ENABLE_FOLDER_BUTTON("enable_folder_button"), ENABLE_QLAUNCH_BUTTON("enable_qlaunch_button"), diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt index a8bd44983b..73ecddad26 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt @@ -720,6 +720,13 @@ abstract class SettingsItem( descriptionId = R.string.gpu_unswizzle_chunk_size_description, choicesId = R.array.gpuSwizzleChunkEntries, valuesId = R.array.gpuSwizzleChunkValues + ) + ) + put( + SwitchSetting( + BooleanSetting.HACK_FIX_SHADOWARRAY, + titleId = R.string.hack_fix_shadowarray, + descriptionId = R.string.hack_fix_shadowarray_description ) ) put( diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt index 332617804e..d130d69ddc 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -287,6 +287,7 @@ class SettingsFragmentPresenter( add(BooleanSetting.FIX_BLOOM_EFFECTS.key) add(BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS.key) add(SettingsItem.GPU_UNSWIZZLE_COMBINED) + add(BooleanSetting.HACK_FIX_SHADOWARRAY.key) add(HeaderSetting(R.string.extensions)) diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index fc1334863d..173f82b4f9 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -524,7 +524,8 @@ GPU Unswizzle Chunk Size Defines the number of depth slices processed per batch for 3D textures. Increasing this improves throughput efficiency on powerful GPUs but may cause stuttering or driver timeouts on weaker hardware. Default - + Fix Shadow Array Handles [EXPERIMENTAL] + Reuse the last valid texture handle within a descriptor array if a slot is null. Fixes black shadows or missing textures in games that rely on array broadcasting (e.g., Cascaded Shadow Maps). Extensions diff --git a/src/common/settings.h b/src/common/settings.h index 7ea4136576..bda1e90498 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -580,6 +580,8 @@ struct Values { "gpu_unswizzle_chunk_size", Category::RendererHacks, Specialization::Default}; + SwitchableSetting hack_fix_shadowarray{linkage, false, "hack_fix_shadowarray", + Category::RendererHacks}; SwitchableSetting gpu_unswizzle_enabled{linkage, false, "gpu_unswizzle_enabled", Category::RendererHacks}; diff --git a/src/qt_common/config/shared_translation.cpp b/src/qt_common/config/shared_translation.cpp index 5f3a6c18c0..ae9029b7de 100644 --- a/src/qt_common/config/shared_translation.cpp +++ b/src/qt_common/config/shared_translation.cpp @@ -318,6 +318,10 @@ std::unique_ptr InitializeTranslations(QObject* parent) tr("GPU Unswizzle Chunk Size"), tr("Determines the number of depth slices processed in a single dispatch.\n" "Increasing this can improve throughput on high-end GPUs but may cause TDR or driver timeouts on weaker hardware.")); + INSERT(Settings, + hack_fix_shadowarray, + tr("Fix Shadow Array Handles [EXPERIMENTAL]"), + tr("Reuse the last valid texture handle within a descriptor array if a slot is null. Fixes black shadows or missing textures in games that rely on array broadcasting (e.g., Cascaded Shadow Maps).")); INSERT(Settings, use_vulkan_driver_pipeline_cache, diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp index d156baa77b..c5fd916689 100644 --- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp +++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp @@ -375,13 +375,37 @@ bool GraphicsPipeline::ConfigureImpl(bool is_indexed) { add_image(desc, false); } } + bool fix_shadows = Settings::values.hack_fix_shadowarray.GetValue(); for (const auto& desc : info.texture_descriptors) { + u32 last_valid_first = 0; + u32 last_valid_second = 0; + for (u32 index = 0; index < desc.count; ++index) { - const auto handle{read_handle(desc, index)}; - views[view_index++] = {handle.first}; + auto handle = read_handle(desc, index); + + if (fix_shadows) { + if (handle.first != 0) { + last_valid_first = handle.first; + last_valid_second = handle.second; + } else if (last_valid_first != 0) { + handle.first = last_valid_first; + handle.second = last_valid_second; + } + } - VideoCommon::SamplerId sampler{texture_cache.GetGraphicsSamplerId(handle.second)}; - samplers[sampler_index++] = sampler; + if (handle.first == 0) { + views[view_index++] = { + .index = 0, + .blacklist = false, + .id = {} + }; + samplers[sampler_index++] = VideoCommon::NULL_SAMPLER_ID; + } else { + views[view_index++] = {handle.first}; + VideoCommon::SamplerId sampler{ + texture_cache.GetGraphicsSamplerId(handle.second)}; + samplers[sampler_index++] = sampler; + } } } if constexpr (Spec::has_images) { @@ -407,6 +431,9 @@ bool GraphicsPipeline::ConfigureImpl(bool is_indexed) { if constexpr (Spec::enabled_stages[4]) { config_stage(4); } + + // Data exists in the slot the shadows want but it seems this outputs a invalid image for all of them. + // The problem is either inside this function or the texture_descriptors above is pulling junk data texture_cache.FillGraphicsImageViews(std::span(views.data(), view_index)); VideoCommon::ImageViewInOut* texture_buffer_it{views.data()}; diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index 71210ffe6e..586d2c8d2c 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -52,8 +52,53 @@ TextureCache

::TextureCache(Runtime& runtime_, Tegra::MaxwellDeviceMemoryManag // Make sure the first index is reserved for the null resources // This way the null resource becomes a compile time constant - void(slot_images.insert(NullImageParams{})); - void(slot_image_views.insert(runtime, NullImageViewParams{})); + if( Settings::values.hack_fix_shadowarray.GetValue() ) { + ImageInfo null_image_info; + null_image_info.type = ImageType::e2D; + null_image_info.format = PixelFormat::A8B8G8R8_UNORM; + null_image_info.size = Extent3D{1, 1, 1}; + null_image_info.resources.levels = 1; + null_image_info.resources.layers = 1; + null_image_info.num_samples = 1; + null_image_info.layer_stride = 0; + null_image_info.maybe_unaligned_layer_stride = 0; + null_image_info.num_samples = 1; + null_image_info.tile_width_spacing = 0; + + const ImageId null_image_id = slot_images.insert(runtime, null_image_info, 0, 0); + + // Upload white pixel (0xFFFFFFFF) to the image + Image& null_image = slot_images[null_image_id]; + auto staging = runtime.UploadStagingBuffer(4); // 1 pixel = 4 bytes + std::memset(staging.mapped_span.data(), 0xFF, 4); // White pixel + + BufferImageCopy copy{ + .buffer_offset = 0, + .buffer_size = 4, + .buffer_row_length = 1, + .buffer_image_height = 1, + .image_subresource = {.base_level = 0, .base_layer = 0, .num_layers = 1}, + .image_offset = {0, 0, 0}, + .image_extent = {1, 1, 1}, + }; + + null_image.UploadMemory(staging, std::array{copy}); + runtime.InsertUploadMemoryBarrier(); + + // Create image view for the null texture + SubresourceRange null_range{ + .base = {.level = 0, .layer = 0}, + .extent = {.levels = 1, .layers = 1}, + }; + ImageViewInfo null_view_info(ImageViewType::e2D, PixelFormat::A8B8G8R8_UNORM, null_range); + + void(slot_image_views.insert(runtime, null_view_info, null_image_id, null_image, slot_images)); + } + else { + void(slot_images.insert(NullImageParams{})); + void(slot_image_views.insert(runtime, NullImageViewParams{})); + } + void(slot_samplers.insert(runtime, sampler_descriptor)); if constexpr (HAS_DEVICE_MEMORY_INFO) {