From 1e23aaf4fcb967f7123df772756d2ce9042d0688 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Wed, 8 Oct 2025 09:11:26 +0900 Subject: [PATCH 01/21] Improve error handling in graphics memory init --- src/xenia/gpu/d3d12/d3d12_shared_memory.cc | 4 +++- src/xenia/gpu/shared_memory.cc | 8 +++++++- src/xenia/gpu/shared_memory.h | 2 +- src/xenia/gpu/vulkan/vulkan_shared_memory.cc | 4 +++- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/xenia/gpu/d3d12/d3d12_shared_memory.cc b/src/xenia/gpu/d3d12/d3d12_shared_memory.cc index f87b61352..84f14787a 100644 --- a/src/xenia/gpu/d3d12/d3d12_shared_memory.cc +++ b/src/xenia/gpu/d3d12/d3d12_shared_memory.cc @@ -34,7 +34,9 @@ D3D12SharedMemory::D3D12SharedMemory(D3D12CommandProcessor& command_processor, D3D12SharedMemory::~D3D12SharedMemory() { Shutdown(true); } bool D3D12SharedMemory::Initialize() { - InitializeCommon(); + if (!InitializeCommon()) { + return false; + } const ui::d3d12::D3D12Provider& provider = command_processor_.GetD3D12Provider(); diff --git a/src/xenia/gpu/shared_memory.cc b/src/xenia/gpu/shared_memory.cc index 79df49d52..e26ef4867 100644 --- a/src/xenia/gpu/shared_memory.cc +++ b/src/xenia/gpu/shared_memory.cc @@ -23,7 +23,7 @@ SharedMemory::SharedMemory(Memory& memory) : memory_(memory) { SharedMemory::~SharedMemory() { ShutdownCommon(); } -void SharedMemory::InitializeCommon() { +bool SharedMemory::InitializeCommon() { size_t num_system_page_flags_entries = ((kBufferSize >> page_size_log2_) + 63) / 64; num_system_page_flags_ = static_cast(num_system_page_flags_entries); @@ -36,6 +36,11 @@ void SharedMemory::InitializeCommon() { nullptr, num_system_page_flags_ * 3 * sizeof(uint64_t), memory::AllocationType::kReserveCommit, memory::PageAccess::kReadWrite); + if (!system_page_flags_base) { + XELOGE("SharedMemory: Failed to allocate system page flags"); + return false; + } + system_page_flags_valid_ = system_page_flags_base, system_page_flags_valid_and_gpu_resolved_ = system_page_flags_base + (num_system_page_flags_), @@ -49,6 +54,7 @@ void SharedMemory::InitializeCommon() { memory_invalidation_callback_handle_ = memory_.RegisterPhysicalMemoryInvalidationCallback( MemoryInvalidationCallbackThunk, this); + return true; } void SharedMemory::InitializeSparseHostGpuMemory(uint32_t granularity_log2) { diff --git a/src/xenia/gpu/shared_memory.h b/src/xenia/gpu/shared_memory.h index b61c1069e..3effe2771 100644 --- a/src/xenia/gpu/shared_memory.h +++ b/src/xenia/gpu/shared_memory.h @@ -105,7 +105,7 @@ class SharedMemory { protected: SharedMemory(Memory& memory); // Call in implementation-specific initialization. - void InitializeCommon(); + bool InitializeCommon(); void InitializeSparseHostGpuMemory(uint32_t granularity_log2); // Call last in implementation-specific shutdown, also callable from the // destructor. diff --git a/src/xenia/gpu/vulkan/vulkan_shared_memory.cc b/src/xenia/gpu/vulkan/vulkan_shared_memory.cc index 76286b938..0c1d03aaa 100644 --- a/src/xenia/gpu/vulkan/vulkan_shared_memory.cc +++ b/src/xenia/gpu/vulkan/vulkan_shared_memory.cc @@ -44,7 +44,9 @@ VulkanSharedMemory::VulkanSharedMemory( VulkanSharedMemory::~VulkanSharedMemory() { Shutdown(true); } bool VulkanSharedMemory::Initialize() { - InitializeCommon(); + if (!InitializeCommon()) { + return false; + } const ui::vulkan::VulkanDevice* const vulkan_device = command_processor_.GetVulkanDevice(); From 2d7ca4fb396b029787dfab3fa0086479ef88fc6c Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Mon, 3 Nov 2025 12:06:59 +0900 Subject: [PATCH 02/21] [GPU] Remove log spam for empty region resolve issues. Affects Forza Horizon 1 and 2, does not seem to be related to any actual rendering issues but makes it very difficult to debug real problems. --- src/xenia/gpu/draw_util.cc | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/xenia/gpu/draw_util.cc b/src/xenia/gpu/draw_util.cc index 0e5805f49..54bf697b8 100644 --- a/src/xenia/gpu/draw_util.cc +++ b/src/xenia/gpu/draw_util.cc @@ -966,10 +966,13 @@ bool GetResolveInfo(const RegisterFile& regs, const Memory& memory, ? 0.5f : 0.0f; int32_t vertices_fixed[6]; + float vertices_swapped[6]; for (size_t i = 0; i < xe::countof(vertices_fixed); ++i) { - vertices_fixed[i] = ui::FloatToD3D11Fixed16p8( - xenos::GpuSwap(vertices_guest[i], fetch.endian) + half_pixel_offset); + vertices_swapped[i] = xenos::GpuSwap(vertices_guest[i], fetch.endian); + vertices_fixed[i] = + ui::FloatToD3D11Fixed16p8(vertices_swapped[i] + half_pixel_offset); } + // Inclusive. int32_t x0 = std::min(std::min(vertices_fixed[0], vertices_fixed[2]), vertices_fixed[4]); @@ -1054,12 +1057,15 @@ bool GetResolveInfo(const RegisterFile& regs, const Memory& memory, xenos::kMaxResolveSize); y1 = y0 + int32_t(xenos::kMaxResolveSize); } - // fails in forza horizon 1 - // x0 is 0, x1 is 0x100, y0 is 0x100, y1 is 0x100 - assert_true(x0 <= x1 && y0 <= y1); + // If the region is empty or inverted after clipping (e.g., entirely outside + // EDRAM bounds due to window offset), treat as a no-op rather than an error. + // The caller checks width/height and skips the resolve. + // Reduces log spam in Forza Horizon 1/2 which seem to do a lot of these + // resolves without any visible impact on rendering. if (x0 >= x1 || y0 >= y1) { - XELOGE("Resolve region is empty"); - return false; + info_out.coordinate_info.width_div_8 = 0; + info_out.height_div_8 = 0; + return true; } info_out.coordinate_info.width_div_8 = From 966d8f0925fa65a5528e9998f238982cb7a7e54b Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:47:44 +0900 Subject: [PATCH 03/21] [Vulkan] fix FBO path reading from output variables for alpha test Makes FBO follow the same pattern as FSI by using function scoped intermediate variables --- src/xenia/gpu/spirv_shader_translator.cc | 92 +++++++++++---------- src/xenia/gpu/spirv_shader_translator.h | 21 ++++- src/xenia/gpu/spirv_shader_translator_rb.cc | 86 +++++++++++-------- 3 files changed, 117 insertions(+), 82 deletions(-) diff --git a/src/xenia/gpu/spirv_shader_translator.cc b/src/xenia/gpu/spirv_shader_translator.cc index 43418bb4e..616304e09 100644 --- a/src/xenia/gpu/spirv_shader_translator.cc +++ b/src/xenia/gpu/spirv_shader_translator.cc @@ -143,6 +143,8 @@ void SpirvShaderTranslator::Reset() { var_main_point_size_edge_flag_kill_vertex_ = spv::NoResult; var_main_kill_pixel_ = spv::NoResult; var_main_fsi_color_written_ = spv::NoResult; + std::fill(output_fragment_data_.begin(), output_fragment_data_.end(), + spv::NoResult); main_switch_op_.reset(); main_switch_next_pc_phi_operands_.clear(); @@ -2234,10 +2236,14 @@ void SpirvShaderTranslator::StartFragmentShaderBeforeMain() { } if (!is_depth_only_fragment_shader_) { - // Framebuffer color attachment outputs. + // Framebuffer color attachment outputs (FBO path only). + // For FBO, we create Output variables here and Function-scoped variables + // in StartFragmentShaderInMain. The Function-scoped variables are used + // throughout the shader (so we can read them for alpha test), and copied + // to the Output variables at the end. if (!edram_fragment_shader_interlock_) { - std::fill(output_or_var_fragment_data_.begin(), - output_or_var_fragment_data_.end(), spv::NoResult); + std::fill(output_fragment_data_.begin(), output_fragment_data_.end(), + spv::NoResult); static const char* const kFragmentDataOutputNames[] = { "xe_out_fragment_data_0", "xe_out_fragment_data_1", @@ -2253,8 +2259,7 @@ void SpirvShaderTranslator::StartFragmentShaderBeforeMain() { spv::Id output_fragment_data_rt = builder_->createVariable( spv::NoPrecision, spv::StorageClassOutput, type_float4_, kFragmentDataOutputNames[color_target_index]); - output_or_var_fragment_data_[color_target_index] = - output_fragment_data_rt; + output_fragment_data_[color_target_index] = output_fragment_data_rt; builder_->addDecoration(output_fragment_data_rt, spv::DecorationLocation, int(color_target_index)); @@ -2311,33 +2316,45 @@ void SpirvShaderTranslator::StartFragmentShaderInMain() { // to the execution mask GPUs naturally have. } + // Initialize color output variables as Function-scoped for both FSI and FBO. + // For FBO, this allows reading the color values back (e.g., for alpha test), + // which isn't possible with Output storage class. The values are copied to + // the actual Output variables at the end of the shader for FBO. + std::fill(output_or_var_fragment_data_.begin(), + output_or_var_fragment_data_.end(), spv::NoResult); + var_main_fsi_color_written_ = spv::NoResult; + uint32_t color_targets_written = current_shader().writes_color_targets(); + if (color_targets_written && !is_depth_only_fragment_shader_) { + static const char* const kFragmentDataVariableNames[] = { + "xe_var_fragment_data_0", + "xe_var_fragment_data_1", + "xe_var_fragment_data_2", + "xe_var_fragment_data_3", + }; + uint32_t color_targets_remaining = color_targets_written; + uint32_t color_target_index; + while (xe::bit_scan_forward(color_targets_remaining, &color_target_index)) { + color_targets_remaining &= ~(UINT32_C(1) << color_target_index); + output_or_var_fragment_data_[color_target_index] = + builder_->createVariable( + spv::NoPrecision, spv::StorageClassFunction, type_float4_, + kFragmentDataVariableNames[color_target_index], const_float4_0_); + } + // Color write tracking for both FSI and FBO paths. + // This is used to conditionally skip alpha test / alpha-to-coverage if + // render target 0 wasn't written on the execution path. + var_main_fsi_color_written_ = builder_->createVariable( + spv::NoPrecision, spv::StorageClassFunction, type_uint_, + "xe_var_color_written", const_uint_0_); + } + if (edram_fragment_shader_interlock_) { - // Initialize color output variables with fragment shader interlock. - std::fill(output_or_var_fragment_data_.begin(), - output_or_var_fragment_data_.end(), spv::NoResult); - var_main_fsi_color_written_ = spv::NoResult; - uint32_t color_targets_written = current_shader().writes_color_targets(); - if (color_targets_written) { - static const char* const kFragmentDataVariableNames[] = { - "xe_var_fragment_data_0", - "xe_var_fragment_data_1", - "xe_var_fragment_data_2", - "xe_var_fragment_data_3", - }; - uint32_t color_targets_remaining = color_targets_written; - uint32_t color_target_index; - while ( - xe::bit_scan_forward(color_targets_remaining, &color_target_index)) { - color_targets_remaining &= ~(UINT32_C(1) << color_target_index); - output_or_var_fragment_data_[color_target_index] = - builder_->createVariable( - spv::NoPrecision, spv::StorageClassFunction, type_float4_, - kFragmentDataVariableNames[color_target_index], - const_float4_0_); - } - var_main_fsi_color_written_ = builder_->createVariable( - spv::NoPrecision, spv::StorageClassFunction, type_uint_, - "xe_var_fsi_color_written", const_uint_0_); + // Initialize depth output variable with fragment shader interlock. + output_or_var_fragment_depth_ = spv::NoResult; + if (current_shader().writes_depth()) { + output_or_var_fragment_depth_ = builder_->createVariable( + spv::NoPrecision, spv::StorageClassFunction, type_float_, + "xe_var_fragment_depth", const_float_0_); } } @@ -2554,16 +2571,6 @@ void SpirvShaderTranslator::StartFragmentShaderInMain() { spv::StorageClassFunction, var_main_registers_, id_vector_temp_)); } - - if (!edram_fragment_shader_interlock_) { - // Initialize the colors for safety. - for (uint32_t i = 0; i < xenos::kMaxColorRenderTargets; ++i) { - spv::Id output_fragment_data_rt = output_or_var_fragment_data_[i]; - if (output_fragment_data_rt != spv::NoResult) { - builder_->createStore(const_float4_0_, output_fragment_data_rt); - } - } - } } void SpirvShaderTranslator::UpdateExecConditionals( @@ -2923,8 +2930,7 @@ void SpirvShaderTranslator::StoreResult(const InstructionResult& result, assert_not_zero(used_write_mask); assert_true(current_shader().writes_color_target(result.storage_index)); target_pointer = output_or_var_fragment_data_[result.storage_index]; - if (edram_fragment_shader_interlock_) { - assert_true(var_main_fsi_color_written_ != spv::NoResult); + if (var_main_fsi_color_written_ != spv::NoResult) { builder_->createStore( builder_->createBinOp( spv::OpBitwiseOr, type_uint_, diff --git a/src/xenia/gpu/spirv_shader_translator.h b/src/xenia/gpu/spirv_shader_translator.h index c73ffb4b3..9a52a8f79 100644 --- a/src/xenia/gpu/spirv_shader_translator.h +++ b/src/xenia/gpu/spirv_shader_translator.h @@ -947,11 +947,23 @@ class SpirvShaderTranslator : public ShaderTranslator { unsigned int output_per_vertex_clip_distance_member_index_ = 0; unsigned int output_per_vertex_cull_distance_member_index_ = 0; - // With fragment shader interlock, variables in the main function. - // Otherwise, framebuffer color attachment outputs. + // Function-scoped variables for fragment color data. + // Used by both FSI and FBO paths so that color values can be read back + // (e.g., for alpha test). For FBO, these are copied to output_fragment_data_ + // at the end of the shader. std::array output_or_var_fragment_data_; + // FBO only: Actual framebuffer color attachment outputs (Output storage). + // These are write-only and populated at the end of the shader from + // output_or_var_fragment_data_. + std::array output_fragment_data_; + + // Fragment shader depth output (gl_FragDepth). + // With fragment shader interlock, a variable in the main function. + // Otherwise, the depth output (only created if shader writes depth). + spv::Id output_or_var_fragment_depth_; + // Fragment shader sample mask output (gl_SampleMask). // Only used for alpha-to-coverage in non-FSI mode. // For FSI mode, sample mask is handled via main_fsi_sample_mask_. @@ -999,11 +1011,12 @@ class SpirvShaderTranslator : public ShaderTranslator { spv::Id var_main_point_size_edge_flag_kill_vertex_; // PS, only when needed - bool. spv::Id var_main_kill_pixel_; - // PS, only when writing to color render targets with fragment shader - // interlock - uint. + // PS, when writing to color render targets - uint. // Whether color buffers have been written to, if not written on the taken // execution path, don't export according to Direct3D 9 register documentation // (some games rely on this behavior). + // Used by both FSI and FBO paths for proper alpha test / alpha-to-coverage + // behavior. spv::Id var_main_fsi_color_written_; // Loaded by FSI_LoadSampleMask. // Can be modified on the outermost control flow level in the main function. diff --git a/src/xenia/gpu/spirv_shader_translator_rb.cc b/src/xenia/gpu/spirv_shader_translator_rb.cc index ec1ccf68b..649b7876a 100644 --- a/src/xenia/gpu/spirv_shader_translator_rb.cc +++ b/src/xenia/gpu/spirv_shader_translator_rb.cc @@ -472,13 +472,15 @@ void SpirvShaderTranslator::CompleteFragmentShaderInMain() { if ((color_targets_written & 0b1) && !IsExecutionModeEarlyFragmentTests()) { spv::Id fsi_sample_mask_in_rt_0_alpha_tests = spv::NoResult; - spv::Block* block_fsi_rt_0_alpha_tests_rt_written_head = nullptr; - spv::Block* block_fsi_rt_0_alpha_tests_rt_written_merge = nullptr; + spv::Block* block_rt_0_alpha_tests_rt_written_head = nullptr; + spv::Block* block_rt_0_alpha_tests_rt_written_merge = nullptr; builder_->makeNewBlock(); - if (edram_fragment_shader_interlock_) { + if (var_main_fsi_color_written_ != spv::NoResult) { // Skip the alpha test and alpha to coverage if the render target 0 is not - // written to dynamically. - fsi_sample_mask_in_rt_0_alpha_tests = main_fsi_sample_mask_; + // written to dynamically. This check is used by both FSI and FBO paths. + if (edram_fragment_shader_interlock_) { + fsi_sample_mask_in_rt_0_alpha_tests = main_fsi_sample_mask_; + } spv::Id rt_0_written = builder_->createBinOp( spv::OpINotEqual, type_bool_, builder_->createBinOp( @@ -487,32 +489,30 @@ void SpirvShaderTranslator::CompleteFragmentShaderInMain() { spv::NoPrecision), builder_->makeUintConstant(0b1)), const_uint_0_); - block_fsi_rt_0_alpha_tests_rt_written_head = builder_->getBuildPoint(); - spv::Block& block_fsi_rt_0_alpha_tests_rt_written = - builder_->makeNewBlock(); - block_fsi_rt_0_alpha_tests_rt_written_merge = &builder_->makeNewBlock(); - builder_->createSelectionMerge( - block_fsi_rt_0_alpha_tests_rt_written_merge, - spv::SelectionControlDontFlattenMask); + block_rt_0_alpha_tests_rt_written_head = builder_->getBuildPoint(); + spv::Block& block_rt_0_alpha_tests_rt_written = builder_->makeNewBlock(); + block_rt_0_alpha_tests_rt_written_merge = &builder_->makeNewBlock(); + builder_->createSelectionMerge(block_rt_0_alpha_tests_rt_written_merge, + spv::SelectionControlDontFlattenMask); { std::unique_ptr rt_0_written_branch_conditional_op = std::make_unique(spv::OpBranchConditional); rt_0_written_branch_conditional_op->addIdOperand(rt_0_written); rt_0_written_branch_conditional_op->addIdOperand( - block_fsi_rt_0_alpha_tests_rt_written.getId()); + block_rt_0_alpha_tests_rt_written.getId()); rt_0_written_branch_conditional_op->addIdOperand( - block_fsi_rt_0_alpha_tests_rt_written_merge->getId()); + block_rt_0_alpha_tests_rt_written_merge->getId()); // More likely to write to the render target 0 than not. rt_0_written_branch_conditional_op->addImmediateOperand(2); rt_0_written_branch_conditional_op->addImmediateOperand(1); builder_->getBuildPoint()->addInstruction( std::move(rt_0_written_branch_conditional_op)); } - block_fsi_rt_0_alpha_tests_rt_written.addPredecessor( - block_fsi_rt_0_alpha_tests_rt_written_head); - block_fsi_rt_0_alpha_tests_rt_written_merge->addPredecessor( - block_fsi_rt_0_alpha_tests_rt_written_head); - builder_->setBuildPoint(&block_fsi_rt_0_alpha_tests_rt_written); + block_rt_0_alpha_tests_rt_written.addPredecessor( + block_rt_0_alpha_tests_rt_written_head); + block_rt_0_alpha_tests_rt_written_merge->addPredecessor( + block_rt_0_alpha_tests_rt_written_head); + builder_->setBuildPoint(&block_rt_0_alpha_tests_rt_written); } // Alpha test. @@ -533,10 +533,9 @@ void SpirvShaderTranslator::CompleteFragmentShaderInMain() { id_vector_temp_.clear(); id_vector_temp_.push_back(builder_->makeIntConstant(3)); spv::Id alpha_test_alpha = builder_->createLoad( - builder_->createAccessChain( - edram_fragment_shader_interlock_ ? spv::StorageClassFunction - : spv::StorageClassOutput, - output_or_var_fragment_data_[0], id_vector_temp_), + builder_->createAccessChain(spv::StorageClassFunction, + output_or_var_fragment_data_[0], + id_vector_temp_), spv::NoPrecision); id_vector_temp_.clear(); id_vector_temp_.push_back( @@ -627,22 +626,23 @@ void SpirvShaderTranslator::CompleteFragmentShaderInMain() { // Alpha to coverage. FSI_AlphaToMask(); - if (edram_fragment_shader_interlock_) { - // Close the render target 0 written check. - builder_->createBranch(block_fsi_rt_0_alpha_tests_rt_written_merge); - spv::Block& block_fsi_rt_0_alpha_tests_rt_written_end = + if (block_rt_0_alpha_tests_rt_written_merge) { + // Close the render target 0 written check (used by both FSI and FBO). + builder_->createBranch(block_rt_0_alpha_tests_rt_written_merge); + spv::Block& block_rt_0_alpha_tests_rt_written_end = *builder_->getBuildPoint(); - builder_->setBuildPoint(block_fsi_rt_0_alpha_tests_rt_written_merge); - if (!features_.demote_to_helper_invocation) { + builder_->setBuildPoint(block_rt_0_alpha_tests_rt_written_merge); + if (edram_fragment_shader_interlock_ && + !features_.demote_to_helper_invocation) { // The tests might have modified the sample mask via // fsi_sample_mask_in_rt_0_alpha_tests. id_vector_temp_.clear(); id_vector_temp_.push_back(fsi_sample_mask_in_rt_0_alpha_tests); id_vector_temp_.push_back( - block_fsi_rt_0_alpha_tests_rt_written_end.getId()); + block_rt_0_alpha_tests_rt_written_end.getId()); id_vector_temp_.push_back(main_fsi_sample_mask_); id_vector_temp_.push_back( - block_fsi_rt_0_alpha_tests_rt_written_head->getId()); + block_rt_0_alpha_tests_rt_written_head->getId()); main_fsi_sample_mask_ = builder_->createOp(spv::OpPhi, type_uint_, id_vector_temp_); } @@ -1297,6 +1297,23 @@ void SpirvShaderTranslator::CompleteFragmentShaderInMain() { } } + if (!edram_fragment_shader_interlock_) { + // FBO path: Copy from Function-scoped variables to Output variables. + // This is done at the end after alpha test/coverage so we can read the + // color values during those operations. + uint32_t color_targets_to_copy = current_shader().writes_color_targets(); + uint32_t color_target_index; + while (xe::bit_scan_forward(color_targets_to_copy, &color_target_index)) { + color_targets_to_copy &= ~(UINT32_C(1) << color_target_index); + spv::Id var_color = output_or_var_fragment_data_[color_target_index]; + spv::Id out_color = output_fragment_data_[color_target_index]; + if (var_color != spv::NoResult && out_color != spv::NoResult) { + builder_->createStore(builder_->createLoad(var_color, spv::NoPrecision), + out_color); + } + } + } + if (edram_fragment_shader_interlock_) { if (block_fsi_if_after_depth_stencil_merge) { builder_->createBranch(block_fsi_if_after_depth_stencil_merge); @@ -3808,10 +3825,9 @@ void SpirvShaderTranslator::FSI_AlphaToMask() { id_vector_temp_.clear(); id_vector_temp_.push_back(builder_->makeIntConstant(3)); // W component spv::Id alpha = builder_->createLoad( - builder_->createAccessChain( - edram_fragment_shader_interlock_ ? spv::StorageClassFunction - : spv::StorageClassOutput, - output_or_var_fragment_data_[0], id_vector_temp_), + builder_->createAccessChain(spv::StorageClassFunction, + output_or_var_fragment_data_[0], + id_vector_temp_), spv::NoPrecision); // Load MSAA sample count to determine which mode to use. From 067641668f1fba4ef89bf1796a555f74b0cc3469 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Tue, 16 Dec 2025 23:54:09 +0900 Subject: [PATCH 04/21] [GPU] Fix min/max blend operations to properly apply factors Applies factors before the MIN/MAX operation - in FSI/ROV via runtime blending, in FBO/RTV by pre-multiplying shader output when dstFactor is ONE only, as we don't have access to dest to provide full support as we do in the FSI/ROV paths. --- src/xenia/gpu/d3d12/pipeline_cache.cc | 32 ++ src/xenia/gpu/dxbc_shader_translator.h | 8 +- src/xenia/gpu/dxbc_shader_translator_om.cc | 382 +++++++++++++++-- src/xenia/gpu/spirv_shader_translator.h | 8 +- src/xenia/gpu/spirv_shader_translator_rb.cc | 396 ++++++++++++------ src/xenia/gpu/vulkan/vulkan_pipeline_cache.cc | 54 +++ 6 files changed, 716 insertions(+), 164 deletions(-) diff --git a/src/xenia/gpu/d3d12/pipeline_cache.cc b/src/xenia/gpu/d3d12/pipeline_cache.cc index 1c1425438..003575b4b 100644 --- a/src/xenia/gpu/d3d12/pipeline_cache.cc +++ b/src/xenia/gpu/d3d12/pipeline_cache.cc @@ -947,6 +947,38 @@ PipelineCache::GetCurrentPixelShaderModification( modification.pixel.depth_stencil_mode = DepthStencilMode::kNoModifiers; } } + + // Check if MIN/MAX blend is used with non-trivial source factors. + // D3D12 fixed-function blend ignores factors for MIN/MAX, but Xbox 360 + // applies them. If the destination factor is ONE (or ZERO), we can + // pre-multiply the shader output by the source factor to emulate this. + // Only RT0 is supported for now. + modification.pixel.rt0_blend_rgb_factor_for_premult = + xenos::BlendFactor::kOne; + modification.pixel.rt0_blend_a_factor_for_premult = + xenos::BlendFactor::kOne; + + if (shader.writes_color_target(0)) { + auto blend_control = regs.Get( + reg::RB_BLENDCONTROL::rt_register_indices[0]); + + // Pre-multiply by kSrcAlpha for MIN/MAX blend ops when dstFactor is ONE. + if ((blend_control.color_comb_fcn == xenos::BlendOp::kMin || + blend_control.color_comb_fcn == xenos::BlendOp::kMax) && + blend_control.color_srcblend == xenos::BlendFactor::kSrcAlpha && + blend_control.color_destblend == xenos::BlendFactor::kOne) { + modification.pixel.rt0_blend_rgb_factor_for_premult = + xenos::BlendFactor::kSrcAlpha; + } + + if ((blend_control.alpha_comb_fcn == xenos::BlendOp::kMin || + blend_control.alpha_comb_fcn == xenos::BlendOp::kMax) && + blend_control.alpha_srcblend == xenos::BlendFactor::kSrcAlpha && + blend_control.alpha_destblend == xenos::BlendFactor::kOne) { + modification.pixel.rt0_blend_a_factor_for_premult = + xenos::BlendFactor::kSrcAlpha; + } + } } return modification; diff --git a/src/xenia/gpu/dxbc_shader_translator.h b/src/xenia/gpu/dxbc_shader_translator.h index 20fbdd328..6af1ce0b0 100644 --- a/src/xenia/gpu/dxbc_shader_translator.h +++ b/src/xenia/gpu/dxbc_shader_translator.h @@ -114,7 +114,7 @@ class DxbcShaderTranslator : public ShaderTranslator { // If anything in this is structure is changed in a way not compatible with // the previous layout, invalidate the pipeline storages by increasing this // version number (0xYYYYMMDD)! - static constexpr uint32_t kVersion = 0x20220720; + static constexpr uint32_t kVersion = 0x20251216; enum class DepthStencilMode : uint32_t { kNoModifiers, @@ -179,6 +179,12 @@ class DxbcShaderTranslator : public ShaderTranslator { uint32_t dynamic_addressable_register_count : 8; // Non-ROV - depth / stencil output mode. DepthStencilMode depth_stencil_mode : 2; + // For host render targets with MIN/MAX blend op - the source blend factor + // to pre-multiply the shader output by (since D3D12 MIN/MAX ignores blend + // factors, but Xbox 360 applies them). kOne means no pre-multiply. + // Only RT0 is supported for now. + xenos::BlendFactor rt0_blend_rgb_factor_for_premult : 5; + xenos::BlendFactor rt0_blend_a_factor_for_premult : 5; } pixel; explicit Modification(uint64_t modification_value = 0) diff --git a/src/xenia/gpu/dxbc_shader_translator_om.cc b/src/xenia/gpu/dxbc_shader_translator_om.cc index e2df54609..001be04b4 100644 --- a/src/xenia/gpu/dxbc_shader_translator_om.cc +++ b/src/xenia/gpu/dxbc_shader_translator_om.cc @@ -1701,6 +1701,143 @@ void DxbcShaderTranslator::CompletePixelShader_WriteToRTVs() { } a_.OpEndIf(); } + // For RT0 with MIN/MAX blend op, pre-multiply by the source blend factor + // (since D3D12 MIN/MAX ignores blend factors, but Xbox 360 applies them). + if (i == 0 && !edram_rov_used_) { + xenos::BlendFactor rgb_factor_for_premult = + GetDxbcShaderModification().pixel.rt0_blend_rgb_factor_for_premult; + xenos::BlendFactor a_factor_for_premult = + GetDxbcShaderModification().pixel.rt0_blend_a_factor_for_premult; + bool premult_rgb = rgb_factor_for_premult != xenos::BlendFactor::kOne; + bool premult_a = a_factor_for_premult != xenos::BlendFactor::kOne; + if (premult_rgb || premult_a) { + uint32_t premult_temp = PushSystemTemp(); + // Compute and apply RGB factor. + if (premult_rgb) { + switch (rgb_factor_for_premult) { + case xenos::BlendFactor::kZero: + a_.OpMov(dxbc::Dest::R(system_temp_color, 0b0111), + dxbc::Src::LF(0.0f)); + break; + case xenos::BlendFactor::kSrcColor: + // Multiply by itself (square). + a_.OpMul(dxbc::Dest::R(system_temp_color, 0b0111), + dxbc::Src::R(system_temp_color), + dxbc::Src::R(system_temp_color)); + break; + case xenos::BlendFactor::kOneMinusSrcColor: + a_.OpAdd(dxbc::Dest::R(premult_temp, 0b0111), dxbc::Src::LF(1.0f), + -dxbc::Src::R(system_temp_color)); + a_.OpMul(dxbc::Dest::R(system_temp_color, 0b0111), + dxbc::Src::R(system_temp_color), + dxbc::Src::R(premult_temp)); + break; + case xenos::BlendFactor::kSrcAlpha: + a_.OpMul(dxbc::Dest::R(system_temp_color, 0b0111), + dxbc::Src::R(system_temp_color), + dxbc::Src::R(system_temp_color, dxbc::Src::kWWWW)); + break; + case xenos::BlendFactor::kOneMinusSrcAlpha: + a_.OpAdd(dxbc::Dest::R(premult_temp, 0b0001), dxbc::Src::LF(1.0f), + -dxbc::Src::R(system_temp_color, dxbc::Src::kWWWW)); + a_.OpMul(dxbc::Dest::R(system_temp_color, 0b0111), + dxbc::Src::R(system_temp_color), + dxbc::Src::R(premult_temp, dxbc::Src::kXXXX)); + break; + case xenos::BlendFactor::kConstantColor: + a_.OpMul(dxbc::Dest::R(system_temp_color, 0b0111), + dxbc::Src::R(system_temp_color), + LoadSystemConstant( + SystemConstants::Index::kEdramBlendConstant, + offsetof(SystemConstants, edram_blend_constant), + dxbc::Src::kXYZW)); + break; + case xenos::BlendFactor::kOneMinusConstantColor: + a_.OpAdd(dxbc::Dest::R(premult_temp, 0b0111), dxbc::Src::LF(1.0f), + -LoadSystemConstant( + SystemConstants::Index::kEdramBlendConstant, + offsetof(SystemConstants, edram_blend_constant), + dxbc::Src::kXYZW)); + a_.OpMul(dxbc::Dest::R(system_temp_color, 0b0111), + dxbc::Src::R(system_temp_color), + dxbc::Src::R(premult_temp)); + break; + case xenos::BlendFactor::kConstantAlpha: + a_.OpMul(dxbc::Dest::R(system_temp_color, 0b0111), + dxbc::Src::R(system_temp_color), + LoadSystemConstant( + SystemConstants::Index::kEdramBlendConstant, + offsetof(SystemConstants, edram_blend_constant), + dxbc::Src::kWWWW)); + break; + case xenos::BlendFactor::kOneMinusConstantAlpha: + a_.OpAdd(dxbc::Dest::R(premult_temp, 0b0001), dxbc::Src::LF(1.0f), + -LoadSystemConstant( + SystemConstants::Index::kEdramBlendConstant, + offsetof(SystemConstants, edram_blend_constant), + dxbc::Src::kWWWW)); + a_.OpMul(dxbc::Dest::R(system_temp_color, 0b0111), + dxbc::Src::R(system_temp_color), + dxbc::Src::R(premult_temp, dxbc::Src::kXXXX)); + break; + default: + // kOne or unsupported - no pre-multiply. + break; + } + } + // Compute and apply alpha factor. + if (premult_a) { + switch (a_factor_for_premult) { + case xenos::BlendFactor::kZero: + a_.OpMov(dxbc::Dest::R(system_temp_color, 0b1000), + dxbc::Src::LF(0.0f)); + break; + case xenos::BlendFactor::kSrcColor: + case xenos::BlendFactor::kSrcAlpha: + // Alpha * Alpha. + a_.OpMul(dxbc::Dest::R(system_temp_color, 0b1000), + dxbc::Src::R(system_temp_color, dxbc::Src::kWWWW), + dxbc::Src::R(system_temp_color, dxbc::Src::kWWWW)); + break; + case xenos::BlendFactor::kOneMinusSrcColor: + case xenos::BlendFactor::kOneMinusSrcAlpha: + a_.OpAdd(dxbc::Dest::R(premult_temp, 0b0001), dxbc::Src::LF(1.0f), + -dxbc::Src::R(system_temp_color, dxbc::Src::kWWWW)); + a_.OpMul(dxbc::Dest::R(system_temp_color, 0b1000), + dxbc::Src::R(system_temp_color, dxbc::Src::kWWWW), + dxbc::Src::R(premult_temp, dxbc::Src::kXXXX)); + break; + case xenos::BlendFactor::kConstantColor: + case xenos::BlendFactor::kConstantAlpha: + a_.OpMul(dxbc::Dest::R(system_temp_color, 0b1000), + dxbc::Src::R(system_temp_color, dxbc::Src::kWWWW), + LoadSystemConstant( + SystemConstants::Index::kEdramBlendConstant, + offsetof(SystemConstants, edram_blend_constant), + dxbc::Src::kWWWW)); + break; + case xenos::BlendFactor::kOneMinusConstantColor: + case xenos::BlendFactor::kOneMinusConstantAlpha: + a_.OpAdd(dxbc::Dest::R(premult_temp, 0b0001), dxbc::Src::LF(1.0f), + -LoadSystemConstant( + SystemConstants::Index::kEdramBlendConstant, + offsetof(SystemConstants, edram_blend_constant), + dxbc::Src::kWWWW)); + a_.OpMul(dxbc::Dest::R(system_temp_color, 0b1000), + dxbc::Src::R(system_temp_color, dxbc::Src::kWWWW), + dxbc::Src::R(premult_temp, dxbc::Src::kXXXX)); + break; + case xenos::BlendFactor::kSrcAlphaSaturate: + // For alpha, SrcAlphaSaturate is 1.0, so no pre-multiply needed. + break; + default: + // kOne or unsupported - no pre-multiply. + break; + } + } + PopSystemTemp(); // premult_temp + } + } // Copy the color from a readable temp register to an output register. a_.OpMov(dxbc::Dest::O(i), dxbc::Src::R(system_temp_color)); } @@ -2526,33 +2663,126 @@ void DxbcShaderTranslator::CompletePixelShader_WriteToROV() { rt_clamp_vec_src.Select(2)); } // Need to do min/max for color. + // Note: Unlike Vulkan/D3D12 fixed-function blend which ignores + // factors for MIN/MAX, the Xbox 360 applies blend factors before + // min/max. a_.OpElse(); { - // Extract the color min (0) or max (1) bit to temp.x - // temp.x = whether min or max should be used for color. + uint32_t blend_src_temp = PushSystemTemp(); + dxbc::Dest blend_src_temp_rgb_dest( + dxbc::Dest::R(blend_src_temp, 0b0111)); + dxbc::Src blend_src_temp_src(dxbc::Src::R(blend_src_temp)); + + // Apply source color factor for min/max. + // Extract the source color factor to temp.x. a_.OpAnd(temp_x_dest, rt_blend_factors_ops_src, - dxbc::Src::LU(1 << 5)); - // Check if need to do min or max for color. - // temp.x = free. + dxbc::Src::LU((1 << 5) - 1)); a_.OpIf(true, temp_x_src); { - // Choose max of the colors without applying the factors to - // color_temp.xyz. - // color_temp.xyz = blended color. - a_.OpMax(color_temp_rgb_dest, - dxbc::Src::R(system_temps_color_[i]), color_temp_src); + a_.OpSwitch(temp_x_src); + ROV_HandleColorBlendFactorCases(system_temps_color_[i], + color_temp, blend_src_temp); + a_.OpEndSwitch(); + // Check if fixed-point and needs clamping. + a_.OpAnd( + temp_x_dest, rt_format_flags_src, + dxbc::Src::LU( + RenderTargetCache::kPSIColorFormatFlag_FixedPointColor)); + a_.OpIf(true, temp_x_src); + { + a_.OpMax(blend_src_temp_rgb_dest, blend_src_temp_src, + rt_clamp_vec_src.Select(0)); + a_.OpMin(blend_src_temp_rgb_dest, blend_src_temp_src, + rt_clamp_vec_src.Select(2)); + } + a_.OpEndIf(); + // Multiply source by factor. + a_.OpMul(blend_src_temp_rgb_dest, + dxbc::Src::R(system_temps_color_[i]), + blend_src_temp_src); + // Clamp result if fixed-point. + a_.OpIf(true, temp_x_src); + { + a_.OpMax(blend_src_temp_rgb_dest, blend_src_temp_src, + rt_clamp_vec_src.Select(0)); + a_.OpMin(blend_src_temp_rgb_dest, blend_src_temp_src, + rt_clamp_vec_src.Select(2)); + } + a_.OpEndIf(); } - // Need to do min. a_.OpElse(); { - // Choose min of the colors without applying the factors to - // color_temp.xyz. - // color_temp.xyz = blended color. - a_.OpMin(color_temp_rgb_dest, - dxbc::Src::R(system_temps_color_[i]), color_temp_src); + a_.OpMov(blend_src_temp_rgb_dest, dxbc::Src::LF(0.0f)); } - // Close the min or max check. a_.OpEndIf(); + + // Apply destination color factor for min/max. + uint32_t blend_dest_temp = PushSystemTemp(); + dxbc::Dest blend_dest_temp_rgb_dest( + dxbc::Dest::R(blend_dest_temp, 0b0111)); + dxbc::Src blend_dest_temp_src(dxbc::Src::R(blend_dest_temp)); + + // Extract the destination color factor to temp.x. + a_.OpUBFE(temp_x_dest, dxbc::Src::LU(5), dxbc::Src::LU(8), + rt_blend_factors_ops_src); + a_.OpIf(true, temp_x_src); + { + a_.OpSwitch(temp_x_src); + ROV_HandleColorBlendFactorCases(system_temps_color_[i], + color_temp, blend_dest_temp); + a_.OpEndSwitch(); + // Check if fixed-point and needs clamping. + a_.OpAnd( + temp_x_dest, rt_format_flags_src, + dxbc::Src::LU( + RenderTargetCache::kPSIColorFormatFlag_FixedPointColor)); + a_.OpIf(true, temp_x_src); + { + a_.OpMax(blend_dest_temp_rgb_dest, blend_dest_temp_src, + rt_clamp_vec_src.Select(0)); + a_.OpMin(blend_dest_temp_rgb_dest, blend_dest_temp_src, + rt_clamp_vec_src.Select(2)); + } + a_.OpEndIf(); + // Multiply destination by factor. + a_.OpMul(blend_dest_temp_rgb_dest, color_temp_src, + blend_dest_temp_src); + // Clamp result if fixed-point. + a_.OpIf(true, temp_x_src); + { + a_.OpMax(blend_dest_temp_rgb_dest, blend_dest_temp_src, + rt_clamp_vec_src.Select(0)); + a_.OpMin(blend_dest_temp_rgb_dest, blend_dest_temp_src, + rt_clamp_vec_src.Select(2)); + } + a_.OpEndIf(); + } + a_.OpElse(); + { + a_.OpMov(blend_dest_temp_rgb_dest, dxbc::Src::LF(0.0f)); + } + a_.OpEndIf(); + + // Now do min or max on the factored values. + // Extract the color min (0) or max (1) bit to temp.x. + a_.OpAnd(temp_x_dest, rt_blend_factors_ops_src, + dxbc::Src::LU(1 << 5)); + a_.OpIf(true, temp_x_src); + { + // MAX: color_temp.xyz = max(src * srcFactor, dst * dstFactor) + a_.OpMax(color_temp_rgb_dest, blend_src_temp_src, + blend_dest_temp_src); + } + a_.OpElse(); + { + // MIN: color_temp.xyz = min(src * srcFactor, dst * dstFactor) + a_.OpMin(color_temp_rgb_dest, blend_src_temp_src, + blend_dest_temp_src); + } + a_.OpEndIf(); + + PopSystemTemp(); // blend_dest_temp + PopSystemTemp(); // blend_src_temp } // Close the color factor blending or min/max check. a_.OpEndIf(); @@ -2731,34 +2961,114 @@ void DxbcShaderTranslator::CompletePixelShader_WriteToROV() { rt_clamp_vec_src.Select(3)); } // Need to do min/max for alpha. + // Note: Unlike Vulkan/D3D12 fixed-function blend which ignores + // factors for MIN/MAX, the Xbox 360 applies blend factors before + // min/max. a_.OpElse(); { - // Extract the alpha min (0) or max (1) bit to temp.x. - // temp.x = whether min or max should be used for alpha. - a_.OpAnd(temp_x_dest, rt_blend_factors_ops_src, - dxbc::Src::LU(1 << 21)); - // Check if need to do min or max for alpha. - // temp.x = free. + // We'll use temp.x for source alpha (factored) and temp.y for + // destination alpha (factored). + + // Apply source alpha factor for min/max. + // Extract the source alpha factor to temp.x (bits 16-20). + a_.OpUBFE(temp_x_dest, dxbc::Src::LU(5), dxbc::Src::LU(16), + rt_blend_factors_ops_src); a_.OpIf(true, temp_x_src); { - // Choose max of the alphas without applying the factors to - // color_temp.w. - // color_temp.w = blended alpha. - a_.OpMax(color_temp_a_dest, + a_.OpSwitch(temp_x_src); + ROV_HandleAlphaBlendFactorCases(system_temps_color_[i], + color_temp, temp, 0); + a_.OpEndSwitch(); + // Check if fixed-point and needs clamping. + uint32_t alpha_is_fixed_temp = PushSystemTemp(); + a_.OpAnd( + dxbc::Dest::R(alpha_is_fixed_temp, 0b0001), + rt_format_flags_src, + dxbc::Src::LU( + RenderTargetCache::kPSIColorFormatFlag_FixedPointAlpha)); + a_.OpIf(true, + dxbc::Src::R(alpha_is_fixed_temp, dxbc::Src::kXXXX)); + { + a_.OpMax(temp_x_dest, temp_x_src, rt_clamp_vec_src.Select(1)); + a_.OpMin(temp_x_dest, temp_x_src, rt_clamp_vec_src.Select(3)); + } + a_.OpEndIf(); + // Multiply source alpha by factor. + a_.OpMul(temp_x_dest, dxbc::Src::R(system_temps_color_[i], dxbc::Src::kWWWW), - color_temp_a_src); + temp_x_src); + // Clamp result if fixed-point. + a_.OpIf(true, + dxbc::Src::R(alpha_is_fixed_temp, dxbc::Src::kXXXX)); + PopSystemTemp(); // alpha_is_fixed_temp + { + a_.OpMax(temp_x_dest, temp_x_src, rt_clamp_vec_src.Select(1)); + a_.OpMin(temp_x_dest, temp_x_src, rt_clamp_vec_src.Select(3)); + } + a_.OpEndIf(); } - // Need to do min. a_.OpElse(); { - // Choose min of the alphas without applying the factors to - // color_temp.w. - // color_temp.w = blended alpha. - a_.OpMin(color_temp_a_dest, - dxbc::Src::R(system_temps_color_[i], dxbc::Src::kWWWW), - color_temp_a_src); + a_.OpMov(temp_x_dest, dxbc::Src::LF(0.0f)); + } + a_.OpEndIf(); + + // Apply destination alpha factor for min/max. + // Extract the destination alpha factor to temp.y (bits 24-28). + a_.OpUBFE(temp_y_dest, dxbc::Src::LU(5), dxbc::Src::LU(24), + rt_blend_factors_ops_src); + a_.OpIf(true, temp_y_src); + { + a_.OpSwitch(temp_y_src); + ROV_HandleAlphaBlendFactorCases(system_temps_color_[i], + color_temp, temp, 1); + a_.OpEndSwitch(); + // Check if fixed-point and needs clamping. + uint32_t alpha_is_fixed_temp2 = PushSystemTemp(); + a_.OpAnd( + dxbc::Dest::R(alpha_is_fixed_temp2, 0b0001), + rt_format_flags_src, + dxbc::Src::LU( + RenderTargetCache::kPSIColorFormatFlag_FixedPointAlpha)); + a_.OpIf(true, + dxbc::Src::R(alpha_is_fixed_temp2, dxbc::Src::kXXXX)); + { + a_.OpMax(temp_y_dest, temp_y_src, rt_clamp_vec_src.Select(1)); + a_.OpMin(temp_y_dest, temp_y_src, rt_clamp_vec_src.Select(3)); + } + a_.OpEndIf(); + // Multiply destination alpha by factor. + a_.OpMul(temp_y_dest, color_temp_a_src, temp_y_src); + // Clamp result if fixed-point. + a_.OpIf(true, + dxbc::Src::R(alpha_is_fixed_temp2, dxbc::Src::kXXXX)); + PopSystemTemp(); // alpha_is_fixed_temp2 + { + a_.OpMax(temp_y_dest, temp_y_src, rt_clamp_vec_src.Select(1)); + a_.OpMin(temp_y_dest, temp_y_src, rt_clamp_vec_src.Select(3)); + } + a_.OpEndIf(); + } + a_.OpElse(); + { + a_.OpMov(temp_y_dest, dxbc::Src::LF(0.0f)); + } + a_.OpEndIf(); + + // Now do min or max on the factored alpha values. + // Extract the alpha min (0) or max (1) bit to color_temp.w. + a_.OpAnd(color_temp_a_dest, rt_blend_factors_ops_src, + dxbc::Src::LU(1 << 21)); + a_.OpIf(true, color_temp_a_src); + { + // MAX: color_temp.w = max(src * srcFactor, dst * dstFactor) + a_.OpMax(color_temp_a_dest, temp_x_src, temp_y_src); + } + a_.OpElse(); + { + // MIN: color_temp.w = min(src * srcFactor, dst * dstFactor) + a_.OpMin(color_temp_a_dest, temp_x_src, temp_y_src); } - // Close the min or max check. a_.OpEndIf(); } // Close the alpha factor blending or min/max check. diff --git a/src/xenia/gpu/spirv_shader_translator.h b/src/xenia/gpu/spirv_shader_translator.h index 9a52a8f79..066b3b5ee 100644 --- a/src/xenia/gpu/spirv_shader_translator.h +++ b/src/xenia/gpu/spirv_shader_translator.h @@ -34,7 +34,7 @@ class SpirvShaderTranslator : public ShaderTranslator { // TODO(Triang3l): Change to 0xYYYYMMDD once it's out of the rapid // prototyping stage (easier to do small granular updates with an // incremental counter). - static constexpr uint32_t kVersion = 7; + static constexpr uint32_t kVersion = 8; enum class DepthStencilMode : uint32_t { kNoModifiers, @@ -83,6 +83,12 @@ class SpirvShaderTranslator : public ShaderTranslator { uint32_t param_gen_point : 1; // For host render targets - depth / stencil output mode. DepthStencilMode depth_stencil_mode : 3; + // For host render targets with MIN/MAX blend op - the source blend factor + // to pre-multiply the shader output by (since Vulkan/D3D12 MIN/MAX + // ignores blend factors, but Xbox 360 applies them). kOne means no + // pre-multiply. Only RT0 is supported for now. + xenos::BlendFactor rt0_blend_rgb_factor_for_premult : 5; + xenos::BlendFactor rt0_blend_a_factor_for_premult : 5; } pixel; uint64_t value = 0; diff --git a/src/xenia/gpu/spirv_shader_translator_rb.cc b/src/xenia/gpu/spirv_shader_translator_rb.cc index 649b7876a..cc600dc8a 100644 --- a/src/xenia/gpu/spirv_shader_translator_rb.cc +++ b/src/xenia/gpu/spirv_shader_translator_rb.cc @@ -1301,6 +1301,12 @@ void SpirvShaderTranslator::CompleteFragmentShaderInMain() { // FBO path: Copy from Function-scoped variables to Output variables. // This is done at the end after alpha test/coverage so we can read the // color values during those operations. + Modification shader_modification = GetSpirvShaderModification(); + xenos::BlendFactor rt0_rgb_premult_factor = + shader_modification.pixel.rt0_blend_rgb_factor_for_premult; + xenos::BlendFactor rt0_a_premult_factor = + shader_modification.pixel.rt0_blend_a_factor_for_premult; + uint32_t color_targets_to_copy = current_shader().writes_color_targets(); uint32_t color_target_index; while (xe::bit_scan_forward(color_targets_to_copy, &color_target_index)) { @@ -1308,8 +1314,175 @@ void SpirvShaderTranslator::CompleteFragmentShaderInMain() { spv::Id var_color = output_or_var_fragment_data_[color_target_index]; spv::Id out_color = output_fragment_data_[color_target_index]; if (var_color != spv::NoResult && out_color != spv::NoResult) { - builder_->createStore(builder_->createLoad(var_color, spv::NoPrecision), - out_color); + spv::Id color = builder_->createLoad(var_color, spv::NoPrecision); + + // For RT0, apply pre-multiply by source blend factor if needed for + // MIN/MAX blend emulation (since Vulkan/D3D12 ignores blend factors + // for MIN/MAX but Xbox 360 applies them). + if (color_target_index == 0 && + (rt0_rgb_premult_factor != xenos::BlendFactor::kOne || + rt0_a_premult_factor != xenos::BlendFactor::kOne)) { + // Helper to extract RGB (xyz) from a float4. + auto extract_rgb = [&](spv::Id vec4) -> spv::Id { + uint_vector_temp_.clear(); + uint_vector_temp_.push_back(0); + uint_vector_temp_.push_back(1); + uint_vector_temp_.push_back(2); + return builder_->createRvalueSwizzle(spv::NoPrecision, type_float3_, + vec4, uint_vector_temp_); + }; + + // Get blend factor values. + auto get_factor_value = [&](xenos::BlendFactor factor, + bool for_alpha) -> spv::Id { + spv::Id src_color = color; + switch (factor) { + case xenos::BlendFactor::kZero: + return for_alpha ? const_float_0_ : const_float3_0_; + case xenos::BlendFactor::kOne: + return for_alpha ? const_float_1_ : const_float3_1_; + case xenos::BlendFactor::kSrcColor: + return for_alpha ? builder_->createCompositeExtract( + src_color, type_float_, 3) + : extract_rgb(src_color); + case xenos::BlendFactor::kOneMinusSrcColor: { + spv::Id src = for_alpha ? builder_->createCompositeExtract( + src_color, type_float_, 3) + : extract_rgb(src_color); + spv::Id one = for_alpha ? const_float_1_ : const_float3_1_; + return builder_->createBinOp( + spv::OpFSub, for_alpha ? type_float_ : type_float3_, one, + src); + } + case xenos::BlendFactor::kSrcAlpha: { + spv::Id alpha = + builder_->createCompositeExtract(src_color, type_float_, 3); + if (for_alpha) { + return alpha; + } + return builder_->smearScalar(spv::NoPrecision, alpha, + type_float3_); + } + case xenos::BlendFactor::kOneMinusSrcAlpha: { + spv::Id alpha = + builder_->createCompositeExtract(src_color, type_float_, 3); + spv::Id one_minus_alpha = builder_->createBinOp( + spv::OpFSub, type_float_, const_float_1_, alpha); + if (for_alpha) { + return one_minus_alpha; + } + return builder_->smearScalar(spv::NoPrecision, one_minus_alpha, + type_float3_); + } + case xenos::BlendFactor::kConstantColor: + case xenos::BlendFactor::kConstantAlpha: { + // Load blend constant from system constants. + id_vector_temp_.clear(); + id_vector_temp_.push_back(builder_->makeIntConstant( + kSystemConstantEdramBlendConstant)); + spv::Id blend_constant = builder_->createLoad( + builder_->createAccessChain(spv::StorageClassUniform, + uniform_system_constants_, + id_vector_temp_), + spv::NoPrecision); + if (factor == xenos::BlendFactor::kConstantAlpha) { + spv::Id alpha = builder_->createCompositeExtract( + blend_constant, type_float_, 3); + if (for_alpha) { + return alpha; + } + return builder_->smearScalar(spv::NoPrecision, alpha, + type_float3_); + } + if (for_alpha) { + return builder_->createCompositeExtract(blend_constant, + type_float_, 3); + } + return extract_rgb(blend_constant); + } + case xenos::BlendFactor::kOneMinusConstantColor: + case xenos::BlendFactor::kOneMinusConstantAlpha: { + id_vector_temp_.clear(); + id_vector_temp_.push_back(builder_->makeIntConstant( + kSystemConstantEdramBlendConstant)); + spv::Id blend_constant = builder_->createLoad( + builder_->createAccessChain(spv::StorageClassUniform, + uniform_system_constants_, + id_vector_temp_), + spv::NoPrecision); + spv::Id constant_value; + if (factor == xenos::BlendFactor::kOneMinusConstantAlpha) { + spv::Id alpha = builder_->createCompositeExtract( + blend_constant, type_float_, 3); + if (for_alpha) { + constant_value = alpha; + } else { + constant_value = builder_->smearScalar(spv::NoPrecision, + alpha, type_float3_); + } + } else { + if (for_alpha) { + constant_value = builder_->createCompositeExtract( + blend_constant, type_float_, 3); + } else { + constant_value = extract_rgb(blend_constant); + } + } + spv::Id one = for_alpha ? const_float_1_ : const_float3_1_; + return builder_->createBinOp( + spv::OpFSub, for_alpha ? type_float_ : type_float3_, one, + constant_value); + } + default: + // Unsupported factors - return 1 (no multiply). + return for_alpha ? const_float_1_ : const_float3_1_; + } + }; + + // Apply RGB pre-multiply. + if (rt0_rgb_premult_factor != xenos::BlendFactor::kOne) { + spv::Id rgb_factor = + get_factor_value(rt0_rgb_premult_factor, false); + spv::Id rgb = extract_rgb(color); + rgb = builder_->createBinOp(spv::OpFMul, type_float3_, rgb, + rgb_factor); + // Reconstruct float4 with new RGB and original alpha. + spv::Id alpha = + builder_->createCompositeExtract(color, type_float_, 3); + id_vector_temp_.clear(); + id_vector_temp_.push_back( + builder_->createCompositeExtract(rgb, type_float_, 0)); + id_vector_temp_.push_back( + builder_->createCompositeExtract(rgb, type_float_, 1)); + id_vector_temp_.push_back( + builder_->createCompositeExtract(rgb, type_float_, 2)); + id_vector_temp_.push_back(alpha); + color = builder_->createCompositeConstruct(type_float4_, + id_vector_temp_); + } + + // Apply alpha pre-multiply. + if (rt0_a_premult_factor != xenos::BlendFactor::kOne) { + spv::Id a_factor = get_factor_value(rt0_a_premult_factor, true); + spv::Id alpha = + builder_->createCompositeExtract(color, type_float_, 3); + alpha = builder_->createBinOp(spv::OpFMul, type_float_, alpha, + a_factor); + // Replace alpha in color (extract RGB, reconstruct with new alpha). + id_vector_temp_.clear(); + id_vector_temp_.push_back( + builder_->createCompositeExtract(color, type_float_, 0)); + id_vector_temp_.push_back( + builder_->createCompositeExtract(color, type_float_, 1)); + id_vector_temp_.push_back( + builder_->createCompositeExtract(color, type_float_, 2)); + id_vector_temp_.push_back(alpha); + color = builder_->createCompositeConstruct(type_float4_, + id_vector_temp_); + } + } + + builder_->createStore(color, out_color); } } } @@ -3346,142 +3519,113 @@ spv::Id SpirvShaderTranslator::FSI_BlendColorOrAlphaWithUnclampedResult( constant_color_clamped != spv::NoResult)); spv::Id value_type = is_alpha ? type_float_ : type_float3_; - // Handle min and max blend operations, which don't involve the factors. - spv::Block& block_min_max_head = *builder_->getBuildPoint(); - spv::Block& block_min_max_min = builder_->makeNewBlock(); - spv::Block& block_min_max_max = builder_->makeNewBlock(); - spv::Block& block_min_max_default = builder_->makeNewBlock(); - spv::Block& block_min_max_merge = builder_->makeNewBlock(); - builder_->createSelectionMerge(&block_min_max_merge, + // Apply blend factors to source and destination first. + // Note: Unlike Vulkan's VK_BLEND_OP_MIN/MAX which ignore blend factors, + // the Xbox 360 applies blend factors before the min/max operation. + // So we apply factors unconditionally, then switch on the equation. + spv::Id term_source, term_dest; + if (is_alpha) { + term_source = FSI_ApplyAlphaBlendFactor( + source_alpha_clamped, is_fixed_point, clamp_min_value, clamp_max_value, + source_factor, source_alpha_clamped, dest_alpha, + constant_alpha_clamped); + term_dest = FSI_ApplyAlphaBlendFactor( + dest_alpha, is_fixed_point, clamp_min_value, clamp_max_value, + dest_factor, source_alpha_clamped, dest_alpha, constant_alpha_clamped); + } else { + term_source = FSI_ApplyColorBlendFactor( + source_color_clamped, is_fixed_point, clamp_min_value, clamp_max_value, + source_factor, source_color_clamped, source_alpha_clamped, dest_color, + dest_alpha, constant_color_clamped, constant_alpha_clamped); + term_dest = FSI_ApplyColorBlendFactor( + dest_color, is_fixed_point, clamp_min_value, clamp_max_value, + dest_factor, source_color_clamped, source_alpha_clamped, dest_color, + dest_alpha, constant_color_clamped, constant_alpha_clamped); + } + + // Now switch on the blend equation to combine the factored terms. + spv::Block& block_equation_head = *builder_->getBuildPoint(); + spv::Block& block_equation_add = builder_->makeNewBlock(); + spv::Block& block_equation_subtract = builder_->makeNewBlock(); + spv::Block& block_equation_rev_subtract = builder_->makeNewBlock(); + spv::Block& block_equation_min = builder_->makeNewBlock(); + spv::Block& block_equation_max = builder_->makeNewBlock(); + spv::Block& block_equation_merge = builder_->makeNewBlock(); + builder_->createSelectionMerge(&block_equation_merge, spv::SelectionControlDontFlattenMask); { - std::unique_ptr min_max_switch_op = + std::unique_ptr equation_switch_op = std::make_unique(spv::OpSwitch); - min_max_switch_op->addIdOperand(equation); - min_max_switch_op->addIdOperand(block_min_max_default.getId()); - min_max_switch_op->addImmediateOperand(int32_t(xenos::BlendOp::kMin)); - min_max_switch_op->addIdOperand(block_min_max_min.getId()); - min_max_switch_op->addImmediateOperand(int32_t(xenos::BlendOp::kMax)); - min_max_switch_op->addIdOperand(block_min_max_max.getId()); - builder_->getBuildPoint()->addInstruction(std::move(min_max_switch_op)); + equation_switch_op->addIdOperand(equation); + // Make addition the default. + equation_switch_op->addIdOperand(block_equation_add.getId()); + equation_switch_op->addImmediateOperand(int32_t(xenos::BlendOp::kSubtract)); + equation_switch_op->addIdOperand(block_equation_subtract.getId()); + equation_switch_op->addImmediateOperand( + int32_t(xenos::BlendOp::kRevSubtract)); + equation_switch_op->addIdOperand(block_equation_rev_subtract.getId()); + equation_switch_op->addImmediateOperand(int32_t(xenos::BlendOp::kMin)); + equation_switch_op->addIdOperand(block_equation_min.getId()); + equation_switch_op->addImmediateOperand(int32_t(xenos::BlendOp::kMax)); + equation_switch_op->addIdOperand(block_equation_max.getId()); + builder_->getBuildPoint()->addInstruction(std::move(equation_switch_op)); } - block_min_max_default.addPredecessor(&block_min_max_head); - block_min_max_min.addPredecessor(&block_min_max_head); - block_min_max_max.addPredecessor(&block_min_max_head); + block_equation_add.addPredecessor(&block_equation_head); + block_equation_subtract.addPredecessor(&block_equation_head); + block_equation_rev_subtract.addPredecessor(&block_equation_head); + block_equation_min.addPredecessor(&block_equation_head); + block_equation_max.addPredecessor(&block_equation_head); + + // Addition case (default). + builder_->setBuildPoint(&block_equation_add); + spv::Id result_add = builder_->createNoContractionBinOp( + spv::OpFAdd, value_type, term_source, term_dest); + builder_->createBranch(&block_equation_merge); + + // Subtraction case. + builder_->setBuildPoint(&block_equation_subtract); + spv::Id result_subtract = builder_->createNoContractionBinOp( + spv::OpFSub, value_type, term_source, term_dest); + builder_->createBranch(&block_equation_merge); + + // Reverse subtraction case. + builder_->setBuildPoint(&block_equation_rev_subtract); + spv::Id result_rev_subtract = builder_->createNoContractionBinOp( + spv::OpFSub, value_type, term_dest, term_source); + builder_->createBranch(&block_equation_merge); // Min case. - builder_->setBuildPoint(&block_min_max_min); - spv::Id result_min = builder_->createBinBuiltinCall( - value_type, ext_inst_glsl_std_450_, GLSLstd450FMin, - is_alpha ? source_alpha_clamped : source_color_clamped, - is_alpha ? dest_alpha : dest_color); - builder_->createBranch(&block_min_max_merge); + builder_->setBuildPoint(&block_equation_min); + spv::Id result_min = + builder_->createBinBuiltinCall(value_type, ext_inst_glsl_std_450_, + GLSLstd450FMin, term_source, term_dest); + builder_->createBranch(&block_equation_merge); // Max case. - builder_->setBuildPoint(&block_min_max_max); - spv::Id result_max = builder_->createBinBuiltinCall( - value_type, ext_inst_glsl_std_450_, GLSLstd450FMax, - is_alpha ? source_alpha_clamped : source_color_clamped, - is_alpha ? dest_alpha : dest_color); - builder_->createBranch(&block_min_max_merge); + builder_->setBuildPoint(&block_equation_max); + spv::Id result_max = + builder_->createBinBuiltinCall(value_type, ext_inst_glsl_std_450_, + GLSLstd450FMax, term_source, term_dest); + builder_->createBranch(&block_equation_merge); - // Blending with factors. - spv::Id result_factors; - { - builder_->setBuildPoint(&block_min_max_default); - - spv::Id term_source, term_dest; - if (is_alpha) { - term_source = FSI_ApplyAlphaBlendFactor( - source_alpha_clamped, is_fixed_point, clamp_min_value, - clamp_max_value, source_factor, source_alpha_clamped, dest_alpha, - constant_alpha_clamped); - term_dest = FSI_ApplyAlphaBlendFactor(dest_alpha, is_fixed_point, - clamp_min_value, clamp_max_value, - dest_factor, source_alpha_clamped, - dest_alpha, constant_alpha_clamped); - } else { - term_source = FSI_ApplyColorBlendFactor( - source_color_clamped, is_fixed_point, clamp_min_value, - clamp_max_value, source_factor, source_color_clamped, - source_alpha_clamped, dest_color, dest_alpha, constant_color_clamped, - constant_alpha_clamped); - term_dest = FSI_ApplyColorBlendFactor( - dest_color, is_fixed_point, clamp_min_value, clamp_max_value, - dest_factor, source_color_clamped, source_alpha_clamped, dest_color, - dest_alpha, constant_color_clamped, constant_alpha_clamped); - } - - spv::Block& block_signs_head = *builder_->getBuildPoint(); - spv::Block& block_signs_add = builder_->makeNewBlock(); - spv::Block& block_signs_subtract = builder_->makeNewBlock(); - spv::Block& block_signs_reverse_subtract = builder_->makeNewBlock(); - spv::Block& block_signs_merge = builder_->makeNewBlock(); - builder_->createSelectionMerge(&block_signs_merge, - spv::SelectionControlDontFlattenMask); - { - std::unique_ptr signs_switch_op = - std::make_unique(spv::OpSwitch); - signs_switch_op->addIdOperand(equation); - // Make addition the default. - signs_switch_op->addIdOperand(block_signs_add.getId()); - signs_switch_op->addImmediateOperand(int32_t(xenos::BlendOp::kSubtract)); - signs_switch_op->addIdOperand(block_signs_subtract.getId()); - signs_switch_op->addImmediateOperand( - int32_t(xenos::BlendOp::kRevSubtract)); - signs_switch_op->addIdOperand(block_signs_reverse_subtract.getId()); - builder_->getBuildPoint()->addInstruction(std::move(signs_switch_op)); - } - block_signs_add.addPredecessor(&block_signs_head); - block_signs_subtract.addPredecessor(&block_signs_head); - block_signs_reverse_subtract.addPredecessor(&block_signs_head); - - // Addition case. - builder_->setBuildPoint(&block_signs_add); - spv::Id result_add = builder_->createNoContractionBinOp( - spv::OpFAdd, value_type, term_source, term_dest); - builder_->createBranch(&block_signs_merge); - - // Subtraction case. - builder_->setBuildPoint(&block_signs_subtract); - spv::Id result_subtract = builder_->createNoContractionBinOp( - spv::OpFSub, value_type, term_source, term_dest); - builder_->createBranch(&block_signs_merge); - - // Reverse subtraction case. - builder_->setBuildPoint(&block_signs_reverse_subtract); - spv::Id result_reverse_subtract = builder_->createNoContractionBinOp( - spv::OpFSub, value_type, term_dest, term_source); - builder_->createBranch(&block_signs_merge); - - // Selection between the signs involved in the addition. - builder_->setBuildPoint(&block_signs_merge); - id_vector_temp_.clear(); - id_vector_temp_.reserve(2 * 3); - id_vector_temp_.push_back(result_add); - id_vector_temp_.push_back(block_signs_add.getId()); - id_vector_temp_.push_back(result_subtract); - id_vector_temp_.push_back(block_signs_subtract.getId()); - id_vector_temp_.push_back(result_reverse_subtract); - id_vector_temp_.push_back(block_signs_reverse_subtract.getId()); - result_factors = - builder_->createOp(spv::OpPhi, value_type, id_vector_temp_); - builder_->createBranch(&block_min_max_merge); - } - // Get the latest block for blending with factors after all the control flow. - spv::Block& block_min_max_default_end = *builder_->getBuildPoint(); - - builder_->setBuildPoint(&block_min_max_merge); - // Choose out of min, max, and blending with factors. + // Merge and create phi for the result. + builder_->setBuildPoint(&block_equation_merge); id_vector_temp_.clear(); - id_vector_temp_.reserve(2 * 3); + id_vector_temp_.push_back(result_add); + id_vector_temp_.push_back(block_equation_add.getId()); + id_vector_temp_.push_back(result_subtract); + id_vector_temp_.push_back(block_equation_subtract.getId()); + id_vector_temp_.push_back(result_rev_subtract); + id_vector_temp_.push_back(block_equation_rev_subtract.getId()); id_vector_temp_.push_back(result_min); - id_vector_temp_.push_back(block_min_max_min.getId()); + id_vector_temp_.push_back(block_equation_min.getId()); id_vector_temp_.push_back(result_max); - id_vector_temp_.push_back(block_min_max_max.getId()); - id_vector_temp_.push_back(result_factors); - id_vector_temp_.push_back(block_min_max_default_end.getId()); - return builder_->createOp(spv::OpPhi, value_type, id_vector_temp_); + id_vector_temp_.push_back(block_equation_max.getId()); + spv::Id result_unclamped = + builder_->createOp(spv::OpPhi, value_type, id_vector_temp_); + + return FSI_FlushNaNClampAndInBlending(result_unclamped, is_fixed_point, + clamp_min_value, clamp_max_value); } void SpirvShaderTranslator::FSI_AlphaToMaskSample( diff --git a/src/xenia/gpu/vulkan/vulkan_pipeline_cache.cc b/src/xenia/gpu/vulkan/vulkan_pipeline_cache.cc index 1e89e5b3d..5ffaded3c 100644 --- a/src/xenia/gpu/vulkan/vulkan_pipeline_cache.cc +++ b/src/xenia/gpu/vulkan/vulkan_pipeline_cache.cc @@ -344,6 +344,38 @@ VulkanPipelineCache::GetCurrentPixelShaderModification( } else { modification.pixel.depth_stencil_mode = DepthStencilMode::kNoModifiers; } + + // Check if MIN/MAX blend is used with non-trivial source factors. + // Vulkan/D3D12 fixed-function blend ignores factors for MIN/MAX, but + // Xbox 360 applies them. If the destination factor is ONE (or ZERO), we can + // pre-multiply the shader output by the source factor to emulate this. + // Only RT0 is supported for now. + modification.pixel.rt0_blend_rgb_factor_for_premult = + xenos::BlendFactor::kOne; + modification.pixel.rt0_blend_a_factor_for_premult = + xenos::BlendFactor::kOne; + + if (shader.writes_color_target(0)) { + auto blend_control = regs.Get( + reg::RB_BLENDCONTROL::rt_register_indices[0]); + + // Pre-multiply by kSrcAlpha for MIN/MAX blend ops when dstFactor is ONE. + if ((blend_control.color_comb_fcn == xenos::BlendOp::kMin || + blend_control.color_comb_fcn == xenos::BlendOp::kMax) && + blend_control.color_srcblend == xenos::BlendFactor::kSrcAlpha && + blend_control.color_destblend == xenos::BlendFactor::kOne) { + modification.pixel.rt0_blend_rgb_factor_for_premult = + xenos::BlendFactor::kSrcAlpha; + } + + if ((blend_control.alpha_comb_fcn == xenos::BlendOp::kMin || + blend_control.alpha_comb_fcn == xenos::BlendOp::kMax) && + blend_control.alpha_srcblend == xenos::BlendFactor::kSrcAlpha && + blend_control.alpha_destblend == xenos::BlendFactor::kOne) { + modification.pixel.rt0_blend_a_factor_for_premult = + xenos::BlendFactor::kSrcAlpha; + } + } } return modification; @@ -2383,6 +2415,16 @@ bool VulkanPipelineCache::EnsurePipelineCreated( VK_BLEND_OP_ADD, VK_BLEND_OP_ADD, VK_BLEND_OP_ADD}; + // Check if the shader pre-multiplies by blend factors for MIN/MAX. + SpirvShaderTranslator::Modification pixel_shader_modification( + description.pixel_shader_modification); + bool rt0_rgb_premult = + pixel_shader_modification.pixel.rt0_blend_rgb_factor_for_premult != + xenos::BlendFactor::kOne; + bool rt0_a_premult = + pixel_shader_modification.pixel.rt0_blend_a_factor_for_premult != + xenos::BlendFactor::kOne; + uint32_t color_rts_remaining = color_rts_used; uint32_t color_rt_index; while (xe::bit_scan_forward(color_rts_remaining, &color_rt_index)) { @@ -2410,6 +2452,18 @@ bool VulkanPipelineCache::EnsurePipelineCreated( kBlendFactorMap[uint32_t(color_rt.dst_alpha_blend_factor)]; color_blend_attachment.alphaBlendOp = kBlendOpMap[uint32_t(color_rt.alpha_blend_op)]; + + // If the shader pre-multiplies by the source blend factor for RT0 + // MIN/MAX, set the pipeline source factor to ONE since it's already + // applied in the shader. + if (color_rt_index == 0) { + if (rt0_rgb_premult) { + color_blend_attachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; + } + if (rt0_a_premult) { + color_blend_attachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + } + } } color_blend_attachment.colorWriteMask = VkColorComponentFlags(color_rt.color_write_mask); From 77852914ffe89d06df5aedc59aeb7dd9f61f0300 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Wed, 31 Dec 2025 14:08:01 +0900 Subject: [PATCH 05/21] [GPU] TextureCache lockless pre-check lockless accessors for base_outdated/mips_outdated and skip lock acquisition when textures appear up to date --- src/xenia/gpu/texture_cache.cc | 22 ++++++++++++++++++++++ src/xenia/gpu/texture_cache.h | 5 +++++ 2 files changed, 27 insertions(+) diff --git a/src/xenia/gpu/texture_cache.cc b/src/xenia/gpu/texture_cache.cc index f043c6468..357e7be99 100644 --- a/src/xenia/gpu/texture_cache.cc +++ b/src/xenia/gpu/texture_cache.cc @@ -693,6 +693,21 @@ void TextureCache::LoadTexturesData(Texture** textures, uint32_t n_textures) { } } + // Lockless pre-check: count how many textures appear outdated. + // If none appear outdated, skip the lock entirely. + uint32_t likely_outdated = 0; + for (uint32_t i = 0; i < n_textures; ++i) { + Texture* current = textures[i]; + if (current->base_outdated_lockless() || + current->mips_outdated_lockless()) { + ++likely_outdated; + } + } + if (likely_outdated == 0) { + // All textures appear up-to-date, skip lock acquisition + return; + } + uint64_t index_base_outdated = 0; uint64_t index_mips_outdated = 0; uint32_t nkept = 0; @@ -811,6 +826,13 @@ void TextureCache::LoadTexturesData(Texture** textures, uint32_t n_textures) { } } bool TextureCache::LoadTextureData(Texture& texture) { + // Lockless pre-check: if texture appears up-to-date, skip the lock. + // This is safe because worst case is a false positive (we acquire lock + // unnecessarily), never a false negative. + if (!texture.base_outdated_lockless() && !texture.mips_outdated_lockless()) { + return true; + } + // Check what needs to be uploaded. bool base_outdated, mips_outdated; { diff --git a/src/xenia/gpu/texture_cache.h b/src/xenia/gpu/texture_cache.h index a66e3af75..4ceafa5aa 100644 --- a/src/xenia/gpu/texture_cache.h +++ b/src/xenia/gpu/texture_cache.h @@ -276,6 +276,11 @@ class TextureCache { bool mips_outdated(const global_unique_lock_type& global_lock) const { return mips_outdated_; } + // Lockless accessors for pre-check optimization. + // Safe to read without lock - worst case is false positive (outdated when + // not). + bool base_outdated_lockless() const { return base_outdated_; } + bool mips_outdated_lockless() const { return mips_outdated_; } void MakeUpToDateAndWatch(const global_unique_lock_type& global_lock); void WatchCallback(const global_unique_lock_type& global_lock, bool is_mip); From bf880fde0c2d89173eea82e61781f2266e2474ad Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Sat, 13 Dec 2025 12:07:34 +0900 Subject: [PATCH 06/21] [Vulkan] Fix alignment related validation errors --- src/xenia/ui/vulkan/vulkan_device.cc | 2 ++ src/xenia/ui/vulkan/vulkan_device.h | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/src/xenia/ui/vulkan/vulkan_device.cc b/src/xenia/ui/vulkan/vulkan_device.cc index dc5b46894..36143a2ff 100644 --- a/src/xenia/ui/vulkan/vulkan_device.cc +++ b/src/xenia/ui/vulkan/vulkan_device.cc @@ -624,6 +624,8 @@ std::unique_ptr VulkanDevice::CreateIfSupported( if (properties.apiVersion >= VK_MAKE_API_VERSION(0, 1, 2, 0)) { if (with_gpu_emulation) { XE_UI_VULKAN_FEATURE_2(features_1_2, samplerMirrorClampToEdge); + XE_UI_VULKAN_FEATURE_2(features_1_2, uniformBufferStandardLayout); + XE_UI_VULKAN_FEATURE_2(features_1_2, scalarBlockLayout); } } else { if (ext_1_2_KHR_sampler_mirror_clamp_to_edge) { diff --git a/src/xenia/ui/vulkan/vulkan_device.h b/src/xenia/ui/vulkan/vulkan_device.h index 7c2ad22cd..30295e358 100644 --- a/src/xenia/ui/vulkan/vulkan_device.h +++ b/src/xenia/ui/vulkan/vulkan_device.h @@ -118,6 +118,14 @@ class VulkanDevice { bool samplerMirrorClampToEdge = false; + // VK_KHR_uniform_buffer_standard_layout (#253, promoted to 1.2) + + bool uniformBufferStandardLayout = false; + + // VK_EXT_scalar_block_layout (#222, promoted to 1.2) + + bool scalarBlockLayout = false; + // VK_KHR_portability_subset (#164) bool constantAlphaColorBlendFactors = false; From 74ce1c812f521cd5f48093bdca38c67cf998891c Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Sat, 13 Dec 2025 12:09:24 +0900 Subject: [PATCH 07/21] [Vulkan] Fix VUID-FragDepth-FragDepth-04216 validation issue --- src/xenia/gpu/spirv_shader_translator.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/xenia/gpu/spirv_shader_translator.cc b/src/xenia/gpu/spirv_shader_translator.cc index 616304e09..b0890eced 100644 --- a/src/xenia/gpu/spirv_shader_translator.cc +++ b/src/xenia/gpu/spirv_shader_translator.cc @@ -723,6 +723,10 @@ std::vector SpirvShaderTranslator::CompleteTranslation() { builder_->addExecutionMode(function_main_, spv::ExecutionModeEarlyFragmentTests); } + if (current_shader().writes_depth()) { + builder_->addExecutionMode(function_main_, + spv::ExecutionModeDepthReplacing); + } if (edram_fragment_shader_interlock_) { // Accessing per-sample values, so interlocking just when there's common // coverage is enough if the device exposes that. From d505b6b53ae5875ba3009f913f969e686f6a45f6 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Wed, 25 Feb 2026 12:20:40 +0900 Subject: [PATCH 08/21] [Vulkan] Create per-swapchain-image present semaphores Fixes vulkan validation issues. --- src/xenia/ui/vulkan/vulkan_presenter.cc | 49 +++++++++++++++++++------ src/xenia/ui/vulkan/vulkan_presenter.h | 3 +- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/src/xenia/ui/vulkan/vulkan_presenter.cc b/src/xenia/ui/vulkan/vulkan_presenter.cc index 648e894cb..a19722545 100644 --- a/src/xenia/ui/vulkan/vulkan_presenter.cc +++ b/src/xenia/ui/vulkan/vulkan_presenter.cc @@ -75,9 +75,6 @@ VulkanPresenter::PaintContext::Submission::~Submission() { dfn.vkDestroyCommandPool(device, draw_command_pool_, nullptr); } - if (present_semaphore_ != VK_NULL_HANDLE) { - dfn.vkDestroySemaphore(device, present_semaphore_, nullptr); - } if (acquire_semaphore_ != VK_NULL_HANDLE) { dfn.vkDestroySemaphore(device, acquire_semaphore_, nullptr); } @@ -98,13 +95,6 @@ bool VulkanPresenter::PaintContext::Submission::Initialize() { "semaphore"); return false; } - if (dfn.vkCreateSemaphore(device, &semaphore_create_info, nullptr, - &present_semaphore_) != VK_SUCCESS) { - XELOGE( - "VulkanPresenter: Failed to create a swapchain image presentation " - "semaphore"); - return false; - } VkCommandPoolCreateInfo command_pool_create_info; command_pool_create_info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; @@ -816,6 +806,29 @@ VulkanPresenter::ConnectOrReconnectPaintingToSurfaceFromUIThread( paint_context_.swapchain_framebuffers.emplace_back(image_view, framebuffer); } + // Create per-swapchain-image present semaphores to avoid + // VUID-vkQueueSubmit-pSignalSemaphores-00067 (semaphore reuse before the + // previous present completes). + paint_context_.swapchain_image_present_semaphores.reserve( + paint_context_.swapchain_images.size()); + VkSemaphoreCreateInfo present_semaphore_create_info; + present_semaphore_create_info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + present_semaphore_create_info.pNext = nullptr; + present_semaphore_create_info.flags = 0; + for (size_t i = 0; i < paint_context_.swapchain_images.size(); ++i) { + VkSemaphore present_semaphore; + if (dfn.vkCreateSemaphore(device, &present_semaphore_create_info, nullptr, + &present_semaphore) != VK_SUCCESS) { + XELOGE( + "VulkanPresenter: Failed to create a per-swapchain-image present " + "semaphore"); + paint_context_.DestroySwapchainAndVulkanSurface(); + return SurfacePaintConnectResult::kFailure; + } + paint_context_.swapchain_image_present_semaphores.push_back( + present_semaphore); + } + is_vsync_implicit_out = paint_context_.swapchain_is_fifo; return SurfacePaintConnectResult::kSuccess; } @@ -1242,6 +1255,13 @@ VkSwapchainKHR VulkanPresenter::PaintContext::CreateSwapchainForVulkanSurface( VkSwapchainKHR VulkanPresenter::PaintContext::PrepareForSwapchainRetirement() { if (swapchain != VK_NULL_HANDLE) { completion_timeline.AwaitAllSubmissions(); + // Also wait for the presentation queue since vkQueuePresentKHR doesn't + // signal a fence, and the present semaphores may still be in use. + if (present_queue_family != UINT32_MAX) { + const VulkanDevice::Queue::Acquisition queue_acquisition = + vulkan_device->AcquireQueue(present_queue_family, 0); + vulkan_device->functions().vkQueueWaitIdle(queue_acquisition.queue()); + } } const VulkanDevice::Functions& dfn = vulkan_device->functions(); const VkDevice device = vulkan_device->device(); @@ -1250,6 +1270,10 @@ VkSwapchainKHR VulkanPresenter::PaintContext::PrepareForSwapchainRetirement() { dfn.vkDestroyImageView(device, framebuffer.image_view, nullptr); } swapchain_framebuffers.clear(); + for (VkSemaphore present_semaphore : swapchain_image_present_semaphores) { + dfn.vkDestroySemaphore(device, present_semaphore, nullptr); + } + swapchain_image_present_semaphores.clear(); swapchain_images.clear(); swapchain_extent.width = 0; swapchain_extent.height = 0; @@ -1386,6 +1410,8 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl( // safe to return early from this function in case of an error. VkSemaphore acquire_semaphore = paint_submission.acquire_semaphore(); + + uint32_t swapchain_image_index; VkResult acquire_result = dfn.vkAcquireNextImageKHR( device, paint_context_.swapchain, UINT64_MAX, acquire_semaphore, @@ -1997,7 +2023,8 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl( paint_context_.ui_setup_command_buffer_current_index = SIZE_MAX; } command_buffers[command_buffer_count++] = draw_command_buffer; - VkSemaphore present_semaphore = paint_submission.present_semaphore(); + VkSemaphore present_semaphore = + paint_context_.swapchain_image_present_semaphores[swapchain_image_index]; VkSubmitInfo submit_info; submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submit_info.pNext = nullptr; diff --git a/src/xenia/ui/vulkan/vulkan_presenter.h b/src/xenia/ui/vulkan/vulkan_presenter.h index 2268a5a2a..ee342d9c7 100644 --- a/src/xenia/ui/vulkan/vulkan_presenter.h +++ b/src/xenia/ui/vulkan/vulkan_presenter.h @@ -297,7 +297,6 @@ class VulkanPresenter final : public Presenter { ~Submission(); VkSemaphore acquire_semaphore() const { return acquire_semaphore_; } - VkSemaphore present_semaphore() const { return present_semaphore_; } VkCommandPool draw_command_pool() const { return draw_command_pool_; } VkCommandBuffer draw_command_buffer() const { return draw_command_buffer_; @@ -310,7 +309,6 @@ class VulkanPresenter final : public Presenter { const VulkanDevice* vulkan_device_; VkSemaphore acquire_semaphore_ = VK_NULL_HANDLE; - VkSemaphore present_semaphore_ = VK_NULL_HANDLE; VkCommandPool draw_command_pool_ = VK_NULL_HANDLE; VkCommandBuffer draw_command_buffer_ = VK_NULL_HANDLE; }; @@ -442,6 +440,7 @@ class VulkanPresenter final : public Presenter { bool swapchain_is_fifo = false; std::vector swapchain_images; std::vector swapchain_framebuffers; + std::vector swapchain_image_present_semaphores; }; explicit VulkanPresenter(HostGpuLossCallback host_gpu_loss_callback, From 6de637b7949cc26edb4bc1b250dfb03761252b0d Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Sun, 14 Dec 2025 00:42:08 +0900 Subject: [PATCH 09/21] [Vulkan] Smear scalar bools. Addresses some shader validation warnings --- src/xenia/gpu/spirv_shader_translator.cc | 6 ++++-- src/xenia/gpu/spirv_shader_translator_memexport.cc | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/xenia/gpu/spirv_shader_translator.cc b/src/xenia/gpu/spirv_shader_translator.cc index b0890eced..e4e7e36e2 100644 --- a/src/xenia/gpu/spirv_shader_translator.cc +++ b/src/xenia/gpu/spirv_shader_translator.cc @@ -3333,7 +3333,8 @@ spv::Id SpirvShaderTranslator::EndianSwap128Uint4(spv::Id value, uint_vector_temp_.push_back(3); uint_vector_temp_.push_back(2); value = builder_->createTriOp( - spv::OpSelect, type_uint4_, is_8in64, + spv::OpSelect, type_uint4_, + builder_->smearScalar(spv::NoPrecision, is_8in64, type_bool4_), builder_->createRvalueSwizzle(spv::NoPrecision, type_uint4_, value, uint_vector_temp_), value); @@ -3348,7 +3349,8 @@ spv::Id SpirvShaderTranslator::EndianSwap128Uint4(spv::Id value, uint_vector_temp_.push_back(1); uint_vector_temp_.push_back(0); value = builder_->createTriOp( - spv::OpSelect, type_uint4_, is_8in128, + spv::OpSelect, type_uint4_, + builder_->smearScalar(spv::NoPrecision, is_8in128, type_bool4_), builder_->createRvalueSwizzle(spv::NoPrecision, type_uint4_, value, uint_vector_temp_), value); diff --git a/src/xenia/gpu/spirv_shader_translator_memexport.cc b/src/xenia/gpu/spirv_shader_translator_memexport.cc index ccfad00cd..2ccb706cd 100644 --- a/src/xenia/gpu/spirv_shader_translator_memexport.cc +++ b/src/xenia/gpu/spirv_shader_translator_memexport.cc @@ -172,9 +172,11 @@ void SpirvShaderTranslator::ExportToMemory(uint8_t export_eM) { uint_vector_temp_.push_back(1); uint_vector_temp_.push_back(0); uint_vector_temp_.push_back(3); + spv::Id swap_red_blue_bool4 = + builder_->smearScalar(spv::NoPrecision, swap_red_blue, type_bool4_); for_each_eM([&](uint32_t eM_index) { eM_swapped[eM_index] = builder_->createTriOp( - spv::OpSelect, type_float4_, swap_red_blue, + spv::OpSelect, type_float4_, swap_red_blue_bool4, builder_->createRvalueSwizzle(spv::NoPrecision, type_float4_, eM_original[eM_index], uint_vector_temp_), eM_original[eM_index]); From 223aa70171839a7afa7154d975d6035f9626eeb6 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Sun, 2 Nov 2025 13:00:29 +0900 Subject: [PATCH 10/21] [Vulkan] Add semaphor reuse workaround and cvar --- src/xenia/ui/vulkan/functions/device_1_0.inc | 1 + src/xenia/ui/vulkan/vulkan_presenter.cc | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/xenia/ui/vulkan/functions/device_1_0.inc b/src/xenia/ui/vulkan/functions/device_1_0.inc index 02d183375..c2c251131 100644 --- a/src/xenia/ui/vulkan/functions/device_1_0.inc +++ b/src/xenia/ui/vulkan/functions/device_1_0.inc @@ -76,6 +76,7 @@ XE_UI_VULKAN_FUNCTION(vkResetDescriptorPool) XE_UI_VULKAN_FUNCTION(vkResetFences) XE_UI_VULKAN_FUNCTION(vkQueueBindSparse) XE_UI_VULKAN_FUNCTION(vkQueueSubmit) +XE_UI_VULKAN_FUNCTION(vkQueueWaitIdle) XE_UI_VULKAN_FUNCTION(vkUnmapMemory) XE_UI_VULKAN_FUNCTION(vkUpdateDescriptorSets) XE_UI_VULKAN_FUNCTION(vkWaitForFences) diff --git a/src/xenia/ui/vulkan/vulkan_presenter.cc b/src/xenia/ui/vulkan/vulkan_presenter.cc index a19722545..7151586c4 100644 --- a/src/xenia/ui/vulkan/vulkan_presenter.cc +++ b/src/xenia/ui/vulkan/vulkan_presenter.cc @@ -48,6 +48,11 @@ DEFINE_bool( "may present with tearing if frames don't meet the host display refresh " "rate.", "Vulkan"); +DEFINE_bool( + vulkan_semaphore_reuse_workaround, false, + "Wait for presentation queue idle before each frame to prevent semaphore " + "reuse. May fix rendering issues but causes significant performance loss.", + "Vulkan"); namespace xe { namespace ui { @@ -1411,6 +1416,18 @@ Presenter::PaintResult VulkanPresenter::PaintAndPresentImpl( VkSemaphore acquire_semaphore = paint_submission.acquire_semaphore(); + // WORKAROUND: Wait for presentation queue to be idle to ensure semaphore + // from previous present is not in use. This prevents + // VUID-vkQueueSubmit-pSignalSemaphores-00067. + // The semaphore is unsignaled by vkQueuePresentKHR, not by submission fences, + // so we must wait for the present queue specifically. + // TODO(has207): Proper fix requires per-swapchain-image semaphores. + // See https://docs.vulkan.org/guide/latest/swapchain_semaphore_reuse.html + if (cvars::vulkan_semaphore_reuse_workaround) { + const VulkanDevice::Queue::Acquisition queue_acquisition = + vulkan_device_->AcquireQueue(paint_context_.present_queue_family, 0); + dfn.vkQueueWaitIdle(queue_acquisition.queue()); + } uint32_t swapchain_image_index; VkResult acquire_result = dfn.vkAcquireNextImageKHR( From 9c37af46210aa445921123c06d6b1da9fd89cd98 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Wed, 3 Dec 2025 15:54:49 +0900 Subject: [PATCH 11/21] [Vulkan] Add proper tracking to swap gamma correction render pass --- src/xenia/gpu/vulkan/vulkan_command_processor.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/xenia/gpu/vulkan/vulkan_command_processor.cc b/src/xenia/gpu/vulkan/vulkan_command_processor.cc index 390ea74fe..417bc8824 100644 --- a/src/xenia/gpu/vulkan/vulkan_command_processor.cc +++ b/src/xenia/gpu/vulkan/vulkan_command_processor.cc @@ -1568,6 +1568,9 @@ void VulkanCommandProcessor::IssueSwap(uint32_t frontbuffer_ptr, render_pass_begin_info.pClearValues = nullptr; deferred_command_buffer_.CmdVkBeginRenderPass( &render_pass_begin_info, VK_SUBPASS_CONTENTS_INLINE); + current_render_pass_ = swap_apply_gamma_render_pass_; + current_framebuffer_ = + nullptr; // Not a render target cache framebuffer VkViewport viewport; viewport.x = 0.0f; @@ -1628,6 +1631,7 @@ void VulkanCommandProcessor::IssueSwap(uint32_t frontbuffer_ptr, deferred_command_buffer_.CmdVkDraw(3, 1, 0, 0); deferred_command_buffer_.CmdVkEndRenderPass(); + current_render_pass_ = VK_NULL_HANDLE; // Insert the release barrier. PushImageMemoryBarrier( From aaf284a4b05e06f03666a147339042d3e180f524 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Mon, 19 Jan 2026 10:49:48 +0900 Subject: [PATCH 12/21] [Vulkan] Fix fullscreen corrupting HDR state on Windows --- src/xenia/ui/vulkan/vulkan_device.cc | 5 +++++ src/xenia/ui/vulkan/vulkan_device.h | 4 ++++ src/xenia/ui/vulkan/vulkan_presenter.cc | 16 ++++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/src/xenia/ui/vulkan/vulkan_device.cc b/src/xenia/ui/vulkan/vulkan_device.cc index 36143a2ff..ab34ce38f 100644 --- a/src/xenia/ui/vulkan/vulkan_device.cc +++ b/src/xenia/ui/vulkan/vulkan_device.cc @@ -167,6 +167,11 @@ std::unique_ptr VulkanDevice::CreateIfSupported( if (with_swapchain) { // #2. XE_UI_VULKAN_STRUCT_EXTENSION(KHR_swapchain) +#if XE_PLATFORM_WIN32 + // #256. Windows-only extension to control fullscreen exclusive behavior. + // Used to prevent HDR state corruption during fullscreen transitions. + XE_UI_VULKAN_STRUCT_EXTENSION(EXT_full_screen_exclusive) +#endif } bool ext_1_2_KHR_sampler_mirror_clamp_to_edge = false; diff --git a/src/xenia/ui/vulkan/vulkan_device.h b/src/xenia/ui/vulkan/vulkan_device.h index 30295e358..f655d4f9d 100644 --- a/src/xenia/ui/vulkan/vulkan_device.h +++ b/src/xenia/ui/vulkan/vulkan_device.h @@ -183,6 +183,10 @@ class VulkanDevice { bool ext_EXT_memory_budget = false; // #238 // Has optional features not implied by this being true. bool ext_1_3_KHR_maintenance4 = false; // #414 +#if XE_PLATFORM_WIN32 + // VK_EXT_full_screen_exclusive (#256, Windows only) + bool ext_EXT_full_screen_exclusive = false; +#endif }; const Extensions& extensions() const { return extensions_; } diff --git a/src/xenia/ui/vulkan/vulkan_presenter.cc b/src/xenia/ui/vulkan/vulkan_presenter.cc index 7151586c4..36f1b55bf 100644 --- a/src/xenia/ui/vulkan/vulkan_presenter.cc +++ b/src/xenia/ui/vulkan/vulkan_presenter.cc @@ -1142,6 +1142,22 @@ VkSwapchainKHR VulkanPresenter::PaintContext::CreateSwapchainForVulkanSurface( VkSwapchainCreateInfoKHR swapchain_create_info; swapchain_create_info.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; swapchain_create_info.pNext = nullptr; + +#if XE_PLATFORM_WIN32 + // On Windows, use VK_EXT_full_screen_exclusive to explicitly disallow + // fullscreen exclusive mode. This prevents HDR state corruption when + // entering/exiting fullscreen, as the Windows compositor remains in control + // of the display state throughout the transition. + VkSurfaceFullScreenExclusiveInfoEXT full_screen_exclusive_info; + if (vulkan_device->extensions().ext_EXT_full_screen_exclusive) { + full_screen_exclusive_info.sType = + VK_STRUCTURE_TYPE_SURFACE_FULL_SCREEN_EXCLUSIVE_INFO_EXT; + full_screen_exclusive_info.pNext = nullptr; + full_screen_exclusive_info.fullScreenExclusive = + VK_FULL_SCREEN_EXCLUSIVE_DISALLOWED_EXT; + swapchain_create_info.pNext = &full_screen_exclusive_info; + } +#endif swapchain_create_info.flags = 0; swapchain_create_info.surface = surface; swapchain_create_info.minImageCount = From bc54720559e7462d1779a7246d02e75a717d746e Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Thu, 26 Feb 2026 10:28:49 +0900 Subject: [PATCH 13/21] [XMA] Fix consume-only context resetting output buffer offsets And remove the now-unused HasTightOutputBuffer heuristic --- src/xenia/apu/xma_context.h | 5 ----- src/xenia/apu/xma_context_new.cc | 16 +++++----------- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/xenia/apu/xma_context.h b/src/xenia/apu/xma_context.h index 9d8a4e847..6cc1563c1 100644 --- a/src/xenia/apu/xma_context.h +++ b/src/xenia/apu/xma_context.h @@ -159,11 +159,6 @@ struct XMA_CONTEXT_DATA { const bool IsConsumeOnlyContext() const { return (input_buffer_0_packet_count | input_buffer_1_packet_count) == 0; } - // Whether the SDC-based minimum exceeds the output buffer size. - const bool HasTightOutputBuffer() const { - return (int32_t)((subframe_decode_count * 2) - 1) > - (int32_t)output_buffer_block_count; - } }; static_assert_size(XMA_CONTEXT_DATA, 64); diff --git a/src/xenia/apu/xma_context_new.cc b/src/xenia/apu/xma_context_new.cc index d714e1438..3fe5a52d0 100644 --- a/src/xenia/apu/xma_context_new.cc +++ b/src/xenia/apu/xma_context_new.cc @@ -130,21 +130,15 @@ bool XmaContextNew::Work() { RingBuffer output_rb = PrepareOutputRingBuffer(&data); if (data.IsConsumeOnlyContext()) { + // Nothing to drain — don't touch the context or we'll reset the + // game's output buffer offsets, causing stale PCM to be re-read. + if (current_frame_remaining_subframes_ == 0) { + return true; + } XELOGAPU("XmaContext {}: Consume-only context, draining subframes", id()); Consume(&output_rb, &data); data.output_buffer_write_offset = output_rb.write_offset() / kOutputBytesPerBlock; - // Clearing contexts that match TightBufferOutput heuristic can disrupt - // playback (e.g. audio noise during races in PGR4), so we only clear - // contexts with enough output buffer headroom where empty input reliably - // indicates the stream is finished (e.g. needed for dialog completion in - // Borderlands 2 startup). - if (!data.HasTightOutputBuffer() && - current_frame_remaining_subframes_ == 0 && output_rb.empty()) { - XELOGAPU("XmaContext {}: Consume-only context fully drained, clearing", - id()); - ClearLocked(&data); - } StoreContextMerged(data, initial_data, context_ptr); return true; } From 26824edcfa4d5f50910d293100385e49c71d78d4 Mon Sep 17 00:00:00 2001 From: Raffaele Date: Fri, 19 Dec 2025 23:47:32 +0100 Subject: [PATCH 14/21] [D3D12] Fix 4D5307DF fur textures - Fixes issue https://github.com/xenia-canary/game-compatibility/issues/6 - Implements a copy texture logic --- src/xenia/gpu/d3d12/d3d12_texture_cache.cc | 214 +++++++++++++++++---- src/xenia/gpu/d3d12/d3d12_texture_cache.h | 15 +- 2 files changed, 191 insertions(+), 38 deletions(-) diff --git a/src/xenia/gpu/d3d12/d3d12_texture_cache.cc b/src/xenia/gpu/d3d12/d3d12_texture_cache.cc index 41fc005bc..0a9d7e608 100644 --- a/src/xenia/gpu/d3d12/d3d12_texture_cache.cc +++ b/src/xenia/gpu/d3d12/d3d12_texture_cache.cc @@ -582,21 +582,42 @@ void D3D12TextureCache::WriteActiveTextureBindfulSRV( const TextureBinding* binding = GetValidTextureBinding(fetch_constant_index); if (binding && AreDimensionsCompatible(host_shader_binding.dimension, binding->key.dimension)) { + bool force_special_view = + (host_shader_binding.dimension == xenos::FetchOpDimension::k2D && + binding->key.dimension == xenos::DataDimension::k3D); + const D3D12TextureBinding& d3d12_binding = d3d12_texture_bindings_[fetch_constant_index]; if (host_shader_binding.is_signed) { // Not supporting signed compressed textures - hopefully DXN and DXT5A are // not used as signed. if (texture_util::IsAnySignSigned(binding->swizzled_signs)) { - descriptor_index = d3d12_binding.descriptor_index_signed; texture = IsSignedVersionSeparateForFormat(binding->key) ? binding->texture_signed : binding->texture; + + if (force_special_view && texture) { + // Request the 2D view of the 3D texture on demand + descriptor_index = FindOrCreateTextureDescriptor( + *static_cast(texture), + xenos::DataDimension::k2DOrStacked, true, binding->host_swizzle); + } else { + descriptor_index = d3d12_binding.descriptor_index_signed; + } } } else { if (texture_util::IsAnySignNotSigned(binding->swizzled_signs)) { - descriptor_index = d3d12_binding.descriptor_index; texture = binding->texture; + + if (force_special_view && texture) { + // Request the 2D view of the 3D texture on demand + descriptor_index = FindOrCreateTextureDescriptor( + *static_cast(texture), + xenos::DataDimension::k2DOrStacked, false, binding->host_swizzle); + } else { + descriptor_index = d3d12_binding.descriptor_index; + texture = binding->texture; + } } } } @@ -646,12 +667,44 @@ uint32_t D3D12TextureCache::GetActiveTextureBindlessSRVIndex( const TextureBinding* binding = GetValidTextureBinding(fetch_constant_index); if (binding && AreDimensionsCompatible(host_shader_binding.dimension, binding->key.dimension)) { + // 3D Texture on 2D Request + bool force_special_view = + (host_shader_binding.dimension == xenos::FetchOpDimension::k2D && + binding->key.dimension == xenos::DataDimension::k3D); + const D3D12TextureBinding& d3d12_binding = d3d12_texture_bindings_[fetch_constant_index]; - descriptor_index = host_shader_binding.is_signed - ? d3d12_binding.descriptor_index_signed - : d3d12_binding.descriptor_index; + + // Helper lambda to get standard index + uint32_t standard_index = host_shader_binding.is_signed + ? d3d12_binding.descriptor_index_signed + : d3d12_binding.descriptor_index; + + if (force_special_view) { + // Determine which texture object to use + Texture* texture = nullptr; + if (host_shader_binding.is_signed) { + texture = IsSignedVersionSeparateForFormat(binding->key) + ? binding->texture_signed + : binding->texture; + if (texture) { + descriptor_index = FindOrCreateTextureDescriptor( + *static_cast(texture), + xenos::DataDimension::k2DOrStacked, true, binding->host_swizzle); + } + } else { + texture = binding->texture; + if (texture) { + descriptor_index = FindOrCreateTextureDescriptor( + *static_cast(texture), + xenos::DataDimension::k2DOrStacked, false, binding->host_swizzle); + } + } + } else { + descriptor_index = standard_index; + } } + if (descriptor_index == UINT32_MAX) { switch (host_shader_binding.dimension) { case xenos::FetchOpDimension::k3DOrStacked: @@ -1756,36 +1809,117 @@ void D3D12TextureCache::UpdateTextureBindingsImpl( if (binding->texture && texture_util::IsAnySignNotSigned(binding->swizzled_signs)) { d3d12_binding.descriptor_index = FindOrCreateTextureDescriptor( - *static_cast(binding->texture), false, - binding->host_swizzle); + *static_cast(binding->texture), + binding->key.dimension, false, binding->host_swizzle); } if (binding->texture_signed && texture_util::IsAnySignSigned(binding->swizzled_signs)) { d3d12_binding.descriptor_index_signed = FindOrCreateTextureDescriptor( - *static_cast(binding->texture_signed), true, - binding->host_swizzle); + *static_cast(binding->texture_signed), + binding->key.dimension, true, binding->host_swizzle); } } else { D3D12Texture* texture = static_cast(binding->texture); if (texture) { if (texture_util::IsAnySignNotSigned(binding->swizzled_signs)) { d3d12_binding.descriptor_index = FindOrCreateTextureDescriptor( - *texture, false, binding->host_swizzle); + *texture, binding->key.dimension, false, binding->host_swizzle); } if (texture_util::IsAnySignSigned(binding->swizzled_signs)) { d3d12_binding.descriptor_index_signed = FindOrCreateTextureDescriptor( - *texture, true, binding->host_swizzle); + *texture, binding->key.dimension, true, binding->host_swizzle); } } } } } +ID3D12Resource* D3D12TextureCache::D3D12Texture::GetOrCreate3DAs2DResource( + D3D12_RESOURCE_STATES end_state) { + auto& d3d12_cache = static_cast(texture_cache()); + + // 1. If it already exists, just transition and return it. + if (resource_3d_as_2d_) { + if (resource_3d_as_2d_state_ != end_state) { + d3d12_cache.command_processor_.PushTransitionBarrier( + resource_3d_as_2d_.Get(), resource_3d_as_2d_state_, end_state); + resource_3d_as_2d_state_ = end_state; + } + return resource_3d_as_2d_.Get(); + } + + // 2. Create the 2D Alias Resource. + D3D12_RESOURCE_DESC source_desc = resource_->GetDesc(); + + D3D12_RESOURCE_DESC desc = source_desc; + desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + desc.DepthOrArraySize = 1; + desc.MipLevels = 1; + + const ui::d3d12::D3D12Provider& provider = + d3d12_cache.command_processor_.GetD3D12Provider(); + ID3D12Device* device = provider.GetDevice(); + + if (FAILED(device->CreateCommittedResource( + &ui::d3d12::util::kHeapPropertiesDefault, + provider.GetHeapFlagCreateNotZeroed(), &desc, + D3D12_RESOURCE_STATE_COPY_DEST, nullptr, + IID_PPV_ARGS(&resource_3d_as_2d_)))) { + XELOGE("D3D12Texture: Failed to create 3D-as-2D alias resource"); + return nullptr; + } + resource_3d_as_2d_state_ = D3D12_RESOURCE_STATE_COPY_DEST; + + // 3. Perform the Copy (Slice 0 of 3D -> 2D). + // Transition MAIN 3D resource to COPY_SOURCE. + D3D12_RESOURCE_STATES old_main_state = + SetResourceState(D3D12_RESOURCE_STATE_COPY_SOURCE); + d3d12_cache.command_processor_.PushTransitionBarrier( + resource(), old_main_state, D3D12_RESOURCE_STATE_COPY_SOURCE); + + auto& command_list = d3d12_cache.command_processor_.GetDeferredCommandList(); + + D3D12_TEXTURE_COPY_LOCATION dest_loc = {}; + dest_loc.pResource = resource_3d_as_2d_.Get(); + dest_loc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + dest_loc.SubresourceIndex = 0; + + D3D12_TEXTURE_COPY_LOCATION src_loc = {}; + src_loc.pResource = resource(); + src_loc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + src_loc.SubresourceIndex = 0; + + // Define the copy box to select ONLY Slice 0 + D3D12_BOX src_box; + src_box.left = 0; + src_box.top = 0; + src_box.front = 0; + src_box.right = static_cast(source_desc.Width); + src_box.bottom = source_desc.Height; + src_box.back = 1; // CRITICAL: Only copy 1 depth slice! + + command_list.D3DCopyTextureRegion(&dest_loc, 0, 0, 0, &src_loc, &src_box); + + // 4. Restore States. + d3d12_cache.command_processor_.PushTransitionBarrier( + resource(), D3D12_RESOURCE_STATE_COPY_SOURCE, old_main_state); + SetResourceState(old_main_state); + + d3d12_cache.command_processor_.PushTransitionBarrier( + resource_3d_as_2d_.Get(), D3D12_RESOURCE_STATE_COPY_DEST, end_state); + resource_3d_as_2d_state_ = end_state; + + return resource_3d_as_2d_.Get(); +} + uint32_t D3D12TextureCache::FindOrCreateTextureDescriptor( - D3D12Texture& texture, bool is_signed, uint32_t host_swizzle) { + D3D12Texture& texture, xenos::DataDimension dimension, bool is_signed, + uint32_t host_swizzle) { D3D12Texture::SRVDescriptorKey descriptor_key; + descriptor_key.key = 0; descriptor_key.is_signed = uint32_t(is_signed); descriptor_key.host_swizzle = host_swizzle; + descriptor_key.dimension = uint32_t(dimension); // Try to find an existing descriptor. uint32_t existing_descriptor_index = @@ -1797,7 +1931,7 @@ uint32_t D3D12TextureCache::FindOrCreateTextureDescriptor( TextureKey texture_key = texture.key(); // Create a new bindless or cached descriptor if supported. - D3D12_SHADER_RESOURCE_VIEW_DESC desc; + D3D12_SHADER_RESOURCE_VIEW_DESC desc = {}; // Zero out struct if (IsSignedVersionSeparateForFormat(texture_key) && texture_key.signed_separate != uint32_t(is_signed)) { @@ -1819,34 +1953,44 @@ uint32_t D3D12TextureCache::FindOrCreateTextureDescriptor( } uint32_t mip_levels = texture_key.mip_max_level + 1; - switch (texture_key.dimension) { - case xenos::DataDimension::k1D: - case xenos::DataDimension::k2DOrStacked: - desc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2DARRAY; + ID3D12Resource* resource_for_view = texture.resource(); + + if (dimension == xenos::DataDimension::k3D) { + desc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE3D; + desc.Texture3D.MostDetailedMip = 0; + desc.Texture3D.MipLevels = mip_levels; + desc.Texture3D.ResourceMinLODClamp = 0.0f; + } else if (dimension == xenos::DataDimension::kCube) { + desc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURECUBE; + desc.TextureCube.MostDetailedMip = 0; + desc.TextureCube.MipLevels = mip_levels; + desc.TextureCube.ResourceMinLODClamp = 0.0f; + } else { + desc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2DARRAY; + if (texture_key.dimension == xenos::DataDimension::k3D) { + resource_for_view = texture.GetOrCreate3DAs2DResource( + D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + if (!resource_for_view) { + return UINT32_MAX; + } + + // Configure SRV for the new 2D resource + desc.Texture2DArray.MostDetailedMip = 0; + desc.Texture2DArray.MipLevels = 1; + desc.Texture2DArray.FirstArraySlice = 0; + desc.Texture2DArray.ArraySize = 1; + desc.Texture2DArray.PlaneSlice = 0; + desc.Texture2DArray.ResourceMinLODClamp = 0.0f; + } else { + // Standard behavior desc.Texture2DArray.MostDetailedMip = 0; desc.Texture2DArray.MipLevels = mip_levels; desc.Texture2DArray.FirstArraySlice = 0; desc.Texture2DArray.ArraySize = texture_key.GetDepthOrArraySize(); desc.Texture2DArray.PlaneSlice = 0; desc.Texture2DArray.ResourceMinLODClamp = 0.0f; - break; - case xenos::DataDimension::k3D: - desc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE3D; - desc.Texture3D.MostDetailedMip = 0; - desc.Texture3D.MipLevels = mip_levels; - desc.Texture3D.ResourceMinLODClamp = 0.0f; - break; - case xenos::DataDimension::kCube: - desc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURECUBE; - desc.TextureCube.MostDetailedMip = 0; - desc.TextureCube.MipLevels = mip_levels; - desc.TextureCube.ResourceMinLODClamp = 0.0f; - break; - default: - assert_unhandled_case(texture_key.dimension); - return UINT32_MAX; + } } - desc.Shader4ComponentMapping = host_swizzle | D3D12_SHADER_COMPONENT_MAPPING_ALWAYS_SET_BIT_AVOIDING_ZEROMEM_MISTAKES; @@ -1894,7 +2038,7 @@ uint32_t D3D12TextureCache::FindOrCreateTextureDescriptor( } } device->CreateShaderResourceView( - texture.resource(), &desc, + resource_for_view, &desc, GetTextureDescriptorCPUHandle(descriptor_index)); texture.AddSRVDescriptorIndex(descriptor_key, descriptor_index); return descriptor_index; diff --git a/src/xenia/gpu/d3d12/d3d12_texture_cache.h b/src/xenia/gpu/d3d12/d3d12_texture_cache.h index e0ff3c60a..c7a125cce 100644 --- a/src/xenia/gpu/d3d12/d3d12_texture_cache.h +++ b/src/xenia/gpu/d3d12/d3d12_texture_cache.h @@ -551,6 +551,7 @@ class D3D12TextureCache final : public TextureCache { struct { uint32_t is_signed : 1; uint32_t host_swizzle : 12; + uint32_t dimension : 2; }; SRVDescriptorKey() : key(0) { static_assert_size(*this, sizeof(key)); } @@ -568,6 +569,8 @@ class D3D12TextureCache final : public TextureCache { } }; + ID3D12Resource* GetOrCreate3DAs2DResource(D3D12_RESOURCE_STATES end_state); + explicit D3D12Texture(D3D12TextureCache& texture_cache, const TextureKey& key, ID3D12Resource* resource, D3D12_RESOURCE_STATES resource_state); @@ -595,6 +598,10 @@ class D3D12TextureCache final : public TextureCache { Microsoft::WRL::ComPtr resource_; D3D12_RESOURCE_STATES resource_state_; + Microsoft::WRL::ComPtr resource_3d_as_2d_; + D3D12_RESOURCE_STATES resource_3d_as_2d_state_ = + D3D12_RESOURCE_STATE_COMMON; + // For bindful - indices in the non-shader-visible descriptor cache for // copying to the shader-visible heap (much faster than recreating, which, // according to profiling, was often a bottleneck in many games). @@ -717,7 +724,8 @@ class D3D12TextureCache final : public TextureCache { case xenos::FetchOpDimension::k1D: case xenos::FetchOpDimension::k2D: return resource_dimension == xenos::DataDimension::k1D || - resource_dimension == xenos::DataDimension::k2DOrStacked; + resource_dimension == xenos::DataDimension::k2DOrStacked || + resource_dimension == xenos::DataDimension::k3D; case xenos::FetchOpDimension::k3DOrStacked: return resource_dimension == xenos::DataDimension::k3D; case xenos::FetchOpDimension::kCube: @@ -730,8 +738,9 @@ class D3D12TextureCache final : public TextureCache { // Returns the index of an existing of a newly created non-shader-visible // cached (for bindful) or a shader-visible global (for bindless) descriptor, // or UINT32_MAX if failed to create. - uint32_t FindOrCreateTextureDescriptor(D3D12Texture& texture, bool is_signed, - uint32_t host_swizzle); + uint32_t FindOrCreateTextureDescriptor(D3D12Texture& texture, + xenos::DataDimension dimension, + bool is_signed, uint32_t host_swizzle); void ReleaseTextureDescriptor(uint32_t descriptor_index); D3D12_CPU_DESCRIPTOR_HANDLE GetTextureDescriptorCPUHandle( uint32_t descriptor_index) const; From fc5ab60a8864fd643b8a242b8e5e47b2d407bac1 Mon Sep 17 00:00:00 2001 From: Raffaele Date: Fri, 19 Dec 2025 23:50:58 +0100 Subject: [PATCH 15/21] [D3D12] Fix 4D5307DF fur textures - Fixes issue https://github.com/xenia-canary/game-compatibility/issues/6 - Implements a load texture logic --- src/xenia/gpu/d3d12/d3d12_texture_cache.cc | 95 ++++++++++------------ src/xenia/gpu/d3d12/d3d12_texture_cache.h | 6 +- 2 files changed, 44 insertions(+), 57 deletions(-) diff --git a/src/xenia/gpu/d3d12/d3d12_texture_cache.cc b/src/xenia/gpu/d3d12/d3d12_texture_cache.cc index 0a9d7e608..936c4d337 100644 --- a/src/xenia/gpu/d3d12/d3d12_texture_cache.cc +++ b/src/xenia/gpu/d3d12/d3d12_texture_cache.cc @@ -1838,78 +1838,65 @@ ID3D12Resource* D3D12TextureCache::D3D12Texture::GetOrCreate3DAs2DResource( D3D12_RESOURCE_STATES end_state) { auto& d3d12_cache = static_cast(texture_cache()); - // 1. If it already exists, just transition and return it. - if (resource_3d_as_2d_) { - if (resource_3d_as_2d_state_ != end_state) { - d3d12_cache.command_processor_.PushTransitionBarrier( - resource_3d_as_2d_.Get(), resource_3d_as_2d_state_, end_state); - resource_3d_as_2d_state_ = end_state; - } - return resource_3d_as_2d_.Get(); + // 1. If cached, transition and return. + if (texture_3d_as_2d_) { + d3d12_cache.command_processor_.PushTransitionBarrier( + texture_3d_as_2d_->resource(), + texture_3d_as_2d_->SetResourceState(end_state), end_state); + return texture_3d_as_2d_->resource(); } - // 2. Create the 2D Alias Resource. - D3D12_RESOURCE_DESC source_desc = resource_->GetDesc(); + // 2. Prepare the Key for loading. + // keep the dimension as k3D. + TextureKey key_load = key(); + key_load.depth_or_array_size_minus_1 = 0; // Force Depth 1 (Slice 0) + key_load.mip_max_level = 0; // Force 1 Mip (Base level only) - D3D12_RESOURCE_DESC desc = source_desc; + // 3. Manually create the 2D D3D12 Resource. + D3D12_RESOURCE_DESC desc = resource_->GetDesc(); desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; desc.DepthOrArraySize = 1; desc.MipLevels = 1; + desc.Alignment = 0; + desc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; + desc.Flags = D3D12_RESOURCE_FLAG_NONE; const ui::d3d12::D3D12Provider& provider = d3d12_cache.command_processor_.GetD3D12Provider(); ID3D12Device* device = provider.GetDevice(); + Microsoft::WRL::ComPtr resource_2d; + // Start in COPY_DEST as LoadTextureData will write to it. + D3D12_RESOURCE_STATES initial_state = D3D12_RESOURCE_STATE_COPY_DEST; + if (FAILED(device->CreateCommittedResource( &ui::d3d12::util::kHeapPropertiesDefault, - provider.GetHeapFlagCreateNotZeroed(), &desc, - D3D12_RESOURCE_STATE_COPY_DEST, nullptr, - IID_PPV_ARGS(&resource_3d_as_2d_)))) { - XELOGE("D3D12Texture: Failed to create 3D-as-2D alias resource"); + provider.GetHeapFlagCreateNotZeroed(), &desc, initial_state, nullptr, + IID_PPV_ARGS(&resource_2d)))) { + XELOGE("D3D12Texture: Failed to create 3D-as-2D resource"); return nullptr; } - resource_3d_as_2d_state_ = D3D12_RESOURCE_STATE_COPY_DEST; - // 3. Perform the Copy (Slice 0 of 3D -> 2D). - // Transition MAIN 3D resource to COPY_SOURCE. - D3D12_RESOURCE_STATES old_main_state = - SetResourceState(D3D12_RESOURCE_STATE_COPY_SOURCE); + // 4. Create the Texture wrapper. + // This wrapper combines a "3D Key" (for correct shader math) + // with a "2D Resource" (for the actual destination storage). + std::unique_ptr texture_wrap(new D3D12Texture( + d3d12_cache, key_load, resource_2d.Get(), initial_state)); + + // 5. Trigger the Load. + if (!d3d12_cache.LoadTextureData(*texture_wrap)) { + XELOGE("D3D12Texture: Failed to untile 3D-as-2D data"); + return nullptr; + } + + // 6. Transition to requested state. d3d12_cache.command_processor_.PushTransitionBarrier( - resource(), old_main_state, D3D12_RESOURCE_STATE_COPY_SOURCE); + texture_wrap->resource(), texture_wrap->SetResourceState(end_state), + end_state); - auto& command_list = d3d12_cache.command_processor_.GetDeferredCommandList(); - - D3D12_TEXTURE_COPY_LOCATION dest_loc = {}; - dest_loc.pResource = resource_3d_as_2d_.Get(); - dest_loc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; - dest_loc.SubresourceIndex = 0; - - D3D12_TEXTURE_COPY_LOCATION src_loc = {}; - src_loc.pResource = resource(); - src_loc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; - src_loc.SubresourceIndex = 0; - - // Define the copy box to select ONLY Slice 0 - D3D12_BOX src_box; - src_box.left = 0; - src_box.top = 0; - src_box.front = 0; - src_box.right = static_cast(source_desc.Width); - src_box.bottom = source_desc.Height; - src_box.back = 1; // CRITICAL: Only copy 1 depth slice! - - command_list.D3DCopyTextureRegion(&dest_loc, 0, 0, 0, &src_loc, &src_box); - - // 4. Restore States. - d3d12_cache.command_processor_.PushTransitionBarrier( - resource(), D3D12_RESOURCE_STATE_COPY_SOURCE, old_main_state); - SetResourceState(old_main_state); - - d3d12_cache.command_processor_.PushTransitionBarrier( - resource_3d_as_2d_.Get(), D3D12_RESOURCE_STATE_COPY_DEST, end_state); - resource_3d_as_2d_state_ = end_state; - - return resource_3d_as_2d_.Get(); + // 7. Cache and return. + texture_3d_as_2d_ = std::move(texture_wrap); + return texture_3d_as_2d_->resource(); } uint32_t D3D12TextureCache::FindOrCreateTextureDescriptor( diff --git a/src/xenia/gpu/d3d12/d3d12_texture_cache.h b/src/xenia/gpu/d3d12/d3d12_texture_cache.h index c7a125cce..d6cae339b 100644 --- a/src/xenia/gpu/d3d12/d3d12_texture_cache.h +++ b/src/xenia/gpu/d3d12/d3d12_texture_cache.h @@ -598,9 +598,9 @@ class D3D12TextureCache final : public TextureCache { Microsoft::WRL::ComPtr resource_; D3D12_RESOURCE_STATES resource_state_; - Microsoft::WRL::ComPtr resource_3d_as_2d_; - D3D12_RESOURCE_STATES resource_3d_as_2d_state_ = - D3D12_RESOURCE_STATE_COMMON; + // Cached 2D view of the first slice, managed as a standalone texture + // object. + std::unique_ptr texture_3d_as_2d_; // For bindful - indices in the non-shader-visible descriptor cache for // copying to the shader-visible heap (much faster than recreating, which, From 7c9b5af2369ee25454c658f63e46ff68f3f9ef5f Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Sun, 11 Jan 2026 16:18:28 +0900 Subject: [PATCH 16/21] [D3D12] add cvar to control 3d-to-2d texture mode --- src/xenia/gpu/d3d12/d3d12_texture_cache.cc | 108 +++++++++++++++------ 1 file changed, 79 insertions(+), 29 deletions(-) diff --git a/src/xenia/gpu/d3d12/d3d12_texture_cache.cc b/src/xenia/gpu/d3d12/d3d12_texture_cache.cc index 936c4d337..648256293 100644 --- a/src/xenia/gpu/d3d12/d3d12_texture_cache.cc +++ b/src/xenia/gpu/d3d12/d3d12_texture_cache.cc @@ -15,6 +15,7 @@ #include #include "xenia/base/assert.h" +#include "xenia/base/cvar.h" #include "xenia/base/logging.h" #include "xenia/base/math.h" #include "xenia/base/profiling.h" @@ -27,6 +28,12 @@ #include "xenia/ui/d3d12/d3d12_upload_buffer_pool.h" #include "xenia/ui/d3d12/d3d12_util.h" +DEFINE_int32(d3d12_3d_to_2d_texture_mode, 0, + "Handle shaders that sample 3D textures as 2D by creating a 2D " + "view of slice 0. 0 = disabled (default), 1 = GPU copy, " + "2 = CPU re-upload from guest memory.", + "D3D12"); + namespace xe { namespace gpu { namespace d3d12 { @@ -616,7 +623,6 @@ void D3D12TextureCache::WriteActiveTextureBindfulSRV( xenos::DataDimension::k2DOrStacked, false, binding->host_swizzle); } else { descriptor_index = d3d12_binding.descriptor_index; - texture = binding->texture; } } } @@ -1836,9 +1842,16 @@ void D3D12TextureCache::UpdateTextureBindingsImpl( ID3D12Resource* D3D12TextureCache::D3D12Texture::GetOrCreate3DAs2DResource( D3D12_RESOURCE_STATES end_state) { + int32_t mode = cvars::d3d12_3d_to_2d_texture_mode; + + // Mode 0: Feature disabled. + if (mode == 0) { + return nullptr; + } + auto& d3d12_cache = static_cast(texture_cache()); - // 1. If cached, transition and return. + // If cached, transition and return. if (texture_3d_as_2d_) { d3d12_cache.command_processor_.PushTransitionBarrier( texture_3d_as_2d_->resource(), @@ -1846,14 +1859,12 @@ ID3D12Resource* D3D12TextureCache::D3D12Texture::GetOrCreate3DAs2DResource( return texture_3d_as_2d_->resource(); } - // 2. Prepare the Key for loading. - // keep the dimension as k3D. - TextureKey key_load = key(); - key_load.depth_or_array_size_minus_1 = 0; // Force Depth 1 (Slice 0) - key_load.mip_max_level = 0; // Force 1 Mip (Base level only) + const ui::d3d12::D3D12Provider& provider = + d3d12_cache.command_processor_.GetD3D12Provider(); + ID3D12Device* device = provider.GetDevice(); - // 3. Manually create the 2D D3D12 Resource. - D3D12_RESOURCE_DESC desc = resource_->GetDesc(); + D3D12_RESOURCE_DESC source_desc = resource_->GetDesc(); + D3D12_RESOURCE_DESC desc = source_desc; desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; desc.DepthOrArraySize = 1; desc.MipLevels = 1; @@ -1861,13 +1872,8 @@ ID3D12Resource* D3D12TextureCache::D3D12Texture::GetOrCreate3DAs2DResource( desc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; desc.Flags = D3D12_RESOURCE_FLAG_NONE; - const ui::d3d12::D3D12Provider& provider = - d3d12_cache.command_processor_.GetD3D12Provider(); - ID3D12Device* device = provider.GetDevice(); - - Microsoft::WRL::ComPtr resource_2d; - // Start in COPY_DEST as LoadTextureData will write to it. D3D12_RESOURCE_STATES initial_state = D3D12_RESOURCE_STATE_COPY_DEST; + Microsoft::WRL::ComPtr resource_2d; if (FAILED(device->CreateCommittedResource( &ui::d3d12::util::kHeapPropertiesDefault, @@ -1877,25 +1883,69 @@ ID3D12Resource* D3D12TextureCache::D3D12Texture::GetOrCreate3DAs2DResource( return nullptr; } - // 4. Create the Texture wrapper. - // This wrapper combines a "3D Key" (for correct shader math) - // with a "2D Resource" (for the actual destination storage). - std::unique_ptr texture_wrap(new D3D12Texture( - d3d12_cache, key_load, resource_2d.Get(), initial_state)); + if (mode == 1) { + // Mode 1: GPU copy - copy slice 0 from the 3D texture to the 2D resource. + D3D12_RESOURCE_STATES old_main_state = + SetResourceState(D3D12_RESOURCE_STATE_COPY_SOURCE); + d3d12_cache.command_processor_.PushTransitionBarrier( + resource(), old_main_state, D3D12_RESOURCE_STATE_COPY_SOURCE); - // 5. Trigger the Load. - if (!d3d12_cache.LoadTextureData(*texture_wrap)) { - XELOGE("D3D12Texture: Failed to untile 3D-as-2D data"); - return nullptr; + auto& command_list = + d3d12_cache.command_processor_.GetDeferredCommandList(); + + D3D12_TEXTURE_COPY_LOCATION dest_loc = {}; + dest_loc.pResource = resource_2d.Get(); + dest_loc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + dest_loc.SubresourceIndex = 0; + + D3D12_TEXTURE_COPY_LOCATION src_loc = {}; + src_loc.pResource = resource(); + src_loc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + src_loc.SubresourceIndex = 0; + + D3D12_BOX src_box; + src_box.left = 0; + src_box.top = 0; + src_box.front = 0; + src_box.right = static_cast(source_desc.Width); + src_box.bottom = source_desc.Height; + src_box.back = 1; // Only copy 1 depth slice. + + command_list.D3DCopyTextureRegion(&dest_loc, 0, 0, 0, &src_loc, &src_box); + + // Restore source state. + d3d12_cache.command_processor_.PushTransitionBarrier( + resource(), D3D12_RESOURCE_STATE_COPY_SOURCE, old_main_state); + SetResourceState(old_main_state); + + // Create a minimal wrapper for the 2D resource. + TextureKey key_2d = key(); + key_2d.depth_or_array_size_minus_1 = 0; + key_2d.mip_max_level = 0; + texture_3d_as_2d_.reset(new D3D12Texture(d3d12_cache, key_2d, + resource_2d.Get(), initial_state)); + } else { + // Mode 2: CPU re-upload - reload texture data from guest memory. + TextureKey key_load = key(); + key_load.depth_or_array_size_minus_1 = 0; + key_load.mip_max_level = 0; + + std::unique_ptr texture_wrap(new D3D12Texture( + d3d12_cache, key_load, resource_2d.Get(), initial_state)); + + if (!d3d12_cache.LoadTextureData(*texture_wrap)) { + XELOGE("D3D12Texture: Failed to untile 3D-as-2D data"); + return nullptr; + } + + texture_3d_as_2d_ = std::move(texture_wrap); } - // 6. Transition to requested state. + // Transition to requested state. d3d12_cache.command_processor_.PushTransitionBarrier( - texture_wrap->resource(), texture_wrap->SetResourceState(end_state), - end_state); + texture_3d_as_2d_->resource(), + texture_3d_as_2d_->SetResourceState(end_state), end_state); - // 7. Cache and return. - texture_3d_as_2d_ = std::move(texture_wrap); return texture_3d_as_2d_->resource(); } From 90c48e1d210ad872f6a415aac4b01d1ef541ffe1 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Tue, 13 Jan 2026 21:33:49 +0900 Subject: [PATCH 17/21] [GPU] 3d-to-d2 texture implementation Adds vulkan version of mode 1 and 2 and fixes related crashes by keeping the 2d texture views from the texture cache. --- src/xenia/gpu/d3d12/d3d12_texture_cache.cc | 22 +- src/xenia/gpu/d3d12/d3d12_texture_cache.h | 4 +- src/xenia/gpu/gpu_flags.cc | 6 + src/xenia/gpu/gpu_flags.h | 2 + src/xenia/gpu/texture_cache.cc | 44 +-- src/xenia/gpu/texture_cache.h | 9 +- .../gpu/vulkan/deferred_command_buffer.cc | 10 + .../gpu/vulkan/deferred_command_buffer.h | 37 +++ src/xenia/gpu/vulkan/vulkan_texture_cache.cc | 277 +++++++++++++++++- src/xenia/gpu/vulkan/vulkan_texture_cache.h | 20 +- src/xenia/ui/vulkan/functions/device_1_0.inc | 1 + 11 files changed, 389 insertions(+), 43 deletions(-) diff --git a/src/xenia/gpu/d3d12/d3d12_texture_cache.cc b/src/xenia/gpu/d3d12/d3d12_texture_cache.cc index 648256293..7a788d641 100644 --- a/src/xenia/gpu/d3d12/d3d12_texture_cache.cc +++ b/src/xenia/gpu/d3d12/d3d12_texture_cache.cc @@ -15,7 +15,6 @@ #include #include "xenia/base/assert.h" -#include "xenia/base/cvar.h" #include "xenia/base/logging.h" #include "xenia/base/math.h" #include "xenia/base/profiling.h" @@ -28,12 +27,6 @@ #include "xenia/ui/d3d12/d3d12_upload_buffer_pool.h" #include "xenia/ui/d3d12/d3d12_util.h" -DEFINE_int32(d3d12_3d_to_2d_texture_mode, 0, - "Handle shaders that sample 3D textures as 2D by creating a 2D " - "view of slice 0. 0 = disabled (default), 1 = GPU copy, " - "2 = CPU re-upload from guest memory.", - "D3D12"); - namespace xe { namespace gpu { namespace d3d12 { @@ -1235,8 +1228,9 @@ ID3D12Resource* D3D12TextureCache::RequestSwapTexture( D3D12TextureCache::D3D12Texture::D3D12Texture( D3D12TextureCache& texture_cache, const TextureKey& key, - ID3D12Resource* resource, D3D12_RESOURCE_STATES resource_state) - : Texture(texture_cache, key), + ID3D12Resource* resource, D3D12_RESOURCE_STATES resource_state, + bool track_usage) + : Texture(texture_cache, key, track_usage), resource_(resource), resource_state_(resource_state) { ID3D12Device* device = @@ -1842,7 +1836,7 @@ void D3D12TextureCache::UpdateTextureBindingsImpl( ID3D12Resource* D3D12TextureCache::D3D12Texture::GetOrCreate3DAs2DResource( D3D12_RESOURCE_STATES end_state) { - int32_t mode = cvars::d3d12_3d_to_2d_texture_mode; + int32_t mode = cvars::gpu_3d_to_2d_texture_mode; // Mode 0: Feature disabled. if (mode == 0) { @@ -1922,16 +1916,18 @@ ID3D12Resource* D3D12TextureCache::D3D12Texture::GetOrCreate3DAs2DResource( TextureKey key_2d = key(); key_2d.depth_or_array_size_minus_1 = 0; key_2d.mip_max_level = 0; - texture_3d_as_2d_.reset(new D3D12Texture(d3d12_cache, key_2d, - resource_2d.Get(), initial_state)); + texture_3d_as_2d_.reset(new D3D12Texture( + d3d12_cache, key_2d, resource_2d.Get(), initial_state, false)); } else { // Mode 2: CPU re-upload - reload texture data from guest memory. + // Keep dimension as k3D so LoadTextureData uses 3D tiling math to correctly + // read slice 0 from the 3D-tiled guest memory. TextureKey key_load = key(); key_load.depth_or_array_size_minus_1 = 0; key_load.mip_max_level = 0; std::unique_ptr texture_wrap(new D3D12Texture( - d3d12_cache, key_load, resource_2d.Get(), initial_state)); + d3d12_cache, key_load, resource_2d.Get(), initial_state, false)); if (!d3d12_cache.LoadTextureData(*texture_wrap)) { XELOGE("D3D12Texture: Failed to untile 3D-as-2D data"); diff --git a/src/xenia/gpu/d3d12/d3d12_texture_cache.h b/src/xenia/gpu/d3d12/d3d12_texture_cache.h index d6cae339b..d03244774 100644 --- a/src/xenia/gpu/d3d12/d3d12_texture_cache.h +++ b/src/xenia/gpu/d3d12/d3d12_texture_cache.h @@ -571,9 +571,11 @@ class D3D12TextureCache final : public TextureCache { ID3D12Resource* GetOrCreate3DAs2DResource(D3D12_RESOURCE_STATES end_state); + // track_usage: if false, texture won't participate in LRU cache eviction. explicit D3D12Texture(D3D12TextureCache& texture_cache, const TextureKey& key, ID3D12Resource* resource, - D3D12_RESOURCE_STATES resource_state); + D3D12_RESOURCE_STATES resource_state, + bool track_usage = true); ~D3D12Texture(); ID3D12Resource* resource() const { return resource_.Get(); } diff --git a/src/xenia/gpu/gpu_flags.cc b/src/xenia/gpu/gpu_flags.cc index 5ab1af8ed..21634dec8 100644 --- a/src/xenia/gpu/gpu_flags.cc +++ b/src/xenia/gpu/gpu_flags.cc @@ -89,3 +89,9 @@ DEFINE_bool(no_discard_stencil_in_transfer_pipelines, false, "Skip stencil bit discard in render target transfer pipelines. " "May improve performance on some GPUs.", "GPU"); + +DEFINE_int32(gpu_3d_to_2d_texture_mode, 2, + "Handle shaders that sample 3D textures as 2D by creating a 2D " + "copy of slice 0. 0 = disabled, 1 = GPU copy, " + "2 = CPU re-upload from guest memory (default).", + "GPU"); diff --git a/src/xenia/gpu/gpu_flags.h b/src/xenia/gpu/gpu_flags.h index bfeefdc53..e73854223 100644 --- a/src/xenia/gpu/gpu_flags.h +++ b/src/xenia/gpu/gpu_flags.h @@ -36,6 +36,8 @@ DECLARE_bool(disassemble_pm4); DECLARE_bool(no_discard_stencil_in_transfer_pipelines); +DECLARE_int32(gpu_3d_to_2d_texture_mode); + #define XE_GPU_FINE_GRAINED_DRAW_SCOPES 1 #endif // XENIA_GPU_GPU_FLAGS_H_ diff --git a/src/xenia/gpu/texture_cache.cc b/src/xenia/gpu/texture_cache.cc index 357e7be99..e0e55aa09 100644 --- a/src/xenia/gpu/texture_cache.cc +++ b/src/xenia/gpu/texture_cache.cc @@ -504,7 +504,7 @@ void TextureCache::Texture::LogAction(const char* action) const { // performed somehow. The list is maintained by the Texture, not the // TextureCache itself (unlike the `textures_` container). TextureCache::Texture::Texture(TextureCache& texture_cache, - const TextureKey& key) + const TextureKey& key, bool track_usage) : texture_cache_(texture_cache), key_(key), guest_layout_(key.GetGuestLayout()), @@ -512,14 +512,17 @@ TextureCache::Texture::Texture(TextureCache& texture_cache, mips_resolved_(key.scaled_resolve), last_usage_submission_index_(texture_cache.current_submission_index_), last_usage_time_(texture_cache.current_submission_time_), - used_previous_(texture_cache.texture_used_last_), - used_next_(nullptr) { - if (texture_cache.texture_used_last_) { - texture_cache.texture_used_last_->used_next_ = this; - } else { - texture_cache.texture_used_first_ = this; + used_previous_(track_usage ? texture_cache.texture_used_last_ : nullptr), + used_next_(nullptr), + in_usage_list_(track_usage) { + if (track_usage) { + if (texture_cache.texture_used_last_) { + texture_cache.texture_used_last_->used_next_ = this; + } else { + texture_cache.texture_used_first_ = this; + } + texture_cache.texture_used_last_ = this; } - texture_cache.texture_used_last_ = this; // Never try to upload data that doesn't exist. base_outdated_ = guest_layout().base.level_data_extent_bytes != 0; @@ -534,15 +537,18 @@ TextureCache::Texture::~Texture() { texture_cache().shared_memory().UnwatchMemoryRange(base_watch_handle_); } - if (used_previous_) { - used_previous_->used_next_ = used_next_; - } else { - texture_cache_.texture_used_first_ = used_next_; - } - if (used_next_) { - used_next_->used_previous_ = used_previous_; - } else { - texture_cache_.texture_used_last_ = used_previous_; + // Only remove from usage list if we were added to it (track_usage=true). + if (in_usage_list_) { + if (used_previous_) { + used_previous_->used_next_ = used_next_; + } else { + texture_cache_.texture_used_first_ = used_next_; + } + if (used_next_) { + used_next_->used_previous_ = used_previous_; + } else { + texture_cache_.texture_used_last_ = used_previous_; + } } texture_cache_.UpdateTexturesTotalHostMemoryUsage(0, host_memory_usage_); @@ -568,6 +574,10 @@ void TextureCache::Texture::MakeUpToDateAndWatch( } void TextureCache::Texture::MarkAsUsed() { + // Textures not in usage tracking (track_usage=false) should not be linked. + if (!in_usage_list_) { + return; + } assert_true(last_usage_submission_index_ <= texture_cache_.current_submission_index_); // This is called very frequently, don't relink unless needed for caching. diff --git a/src/xenia/gpu/texture_cache.h b/src/xenia/gpu/texture_cache.h index 4ceafa5aa..a8d1729dd 100644 --- a/src/xenia/gpu/texture_cache.h +++ b/src/xenia/gpu/texture_cache.h @@ -294,7 +294,11 @@ class TextureCache { void LogAction(const char* action) const; protected: - explicit Texture(TextureCache& texture_cache, const TextureKey& key); + // track_usage: if false, the texture won't be added to the LRU tracking + // list. Use this for wrapper textures that shouldn't participate in cache + // eviction (like texture_3d_as_2d_ wrappers). + explicit Texture(TextureCache& texture_cache, const TextureKey& key, + bool track_usage = true); void SetHostMemoryUsage(uint64_t new_host_memory_usage) { texture_cache_.UpdateTexturesTotalHostMemoryUsage(new_host_memory_usage, @@ -315,6 +319,9 @@ class TextureCache { uint64_t last_usage_time_; Texture* used_previous_; Texture* used_next_; + // Whether this texture is in the usage tracking list (for LRU eviction). + // Set to false via constructor for wrapper textures. + bool in_usage_list_; // Whether the most up-to-date base / mips contain pages with data from a // resolve operation (rather than from the CPU or memexport), primarily for diff --git a/src/xenia/gpu/vulkan/deferred_command_buffer.cc b/src/xenia/gpu/vulkan/deferred_command_buffer.cc index 0ddfef765..b533b625d 100644 --- a/src/xenia/gpu/vulkan/deferred_command_buffer.cc +++ b/src/xenia/gpu/vulkan/deferred_command_buffer.cc @@ -175,6 +175,16 @@ void DeferredCommandBuffer::Execute(VkCommandBuffer command_buffer) { args.filter); } break; + case Command::kVkCopyImage: { + auto& args = *reinterpret_cast(stream); + dfn.vkCmdCopyImage( + command_buffer, args.src_image, args.src_image_layout, + args.dst_image, args.dst_image_layout, args.region_count, + reinterpret_cast( + reinterpret_cast(stream) + + xe::align(sizeof(ArgsVkCopyImage), alignof(VkImageCopy)))); + } break; + case Command::kVkDispatch: { auto& args = *reinterpret_cast(stream); dfn.vkCmdDispatch(command_buffer, args.group_count_x, diff --git a/src/xenia/gpu/vulkan/deferred_command_buffer.h b/src/xenia/gpu/vulkan/deferred_command_buffer.h index 4fef1ee6f..8352e06d4 100644 --- a/src/xenia/gpu/vulkan/deferred_command_buffer.h +++ b/src/xenia/gpu/vulkan/deferred_command_buffer.h @@ -260,6 +260,32 @@ class DeferredCommandBuffer { regions, sizeof(VkImageBlit) * region_count); } + VkImageCopy* CmdCopyImageEmplace(VkImage src_image, + VkImageLayout src_image_layout, + VkImage dst_image, + VkImageLayout dst_image_layout, + uint32_t region_count) { + const size_t header_size = + xe::align(sizeof(ArgsVkCopyImage), alignof(VkImageCopy)); + uint8_t* args_ptr = reinterpret_cast( + WriteCommand(Command::kVkCopyImage, + header_size + sizeof(VkImageCopy) * region_count)); + auto& args = *reinterpret_cast(args_ptr); + args.src_image = src_image; + args.src_image_layout = src_image_layout; + args.dst_image = dst_image; + args.dst_image_layout = dst_image_layout; + args.region_count = region_count; + return reinterpret_cast(args_ptr + header_size); + } + void CmdVkCopyImage(VkImage src_image, VkImageLayout src_image_layout, + VkImage dst_image, VkImageLayout dst_image_layout, + uint32_t region_count, const VkImageCopy* regions) { + std::memcpy(CmdCopyImageEmplace(src_image, src_image_layout, dst_image, + dst_image_layout, region_count), + regions, sizeof(VkImageCopy) * region_count); + } + void CmdVkDispatch(uint32_t group_count_x, uint32_t group_count_y, uint32_t group_count_z) { auto& args = *reinterpret_cast( @@ -398,6 +424,7 @@ class DeferredCommandBuffer { kVkCopyBuffer, kVkCopyBufferToImage, kVkBlitImage, + kVkCopyImage, kVkDispatch, kVkDraw, kVkDrawIndexed, @@ -504,6 +531,16 @@ class DeferredCommandBuffer { static_assert(alignof(VkImageBlit) <= alignof(uintmax_t)); }; + struct ArgsVkCopyImage { + VkImage src_image; + VkImageLayout src_image_layout; + VkImage dst_image; + VkImageLayout dst_image_layout; + uint32_t region_count; + // Followed by aligned VkImageCopy[]. + static_assert(alignof(VkImageCopy) <= alignof(uintmax_t)); + }; + struct ArgsVkDispatch { uint32_t group_count_x; uint32_t group_count_y; diff --git a/src/xenia/gpu/vulkan/vulkan_texture_cache.cc b/src/xenia/gpu/vulkan/vulkan_texture_cache.cc index 26cd9123a..79d581f90 100644 --- a/src/xenia/gpu/vulkan/vulkan_texture_cache.cc +++ b/src/xenia/gpu/vulkan/vulkan_texture_cache.cc @@ -605,14 +605,34 @@ void VulkanTextureCache::RequestTextures(uint32_t used_texture_mask) { VkImageView VulkanTextureCache::GetActiveBindingOrNullImageView( uint32_t fetch_constant_index, xenos::FetchOpDimension dimension, - bool is_signed) const { + bool is_signed) { VkImageView image_view = VK_NULL_HANDLE; const TextureBinding* binding = GetValidTextureBinding(fetch_constant_index); if (binding && AreDimensionsCompatible(dimension, binding->key.dimension)) { - const VulkanTextureBinding& vulkan_binding = - vulkan_texture_bindings_[fetch_constant_index]; - image_view = is_signed ? vulkan_binding.image_view_signed - : vulkan_binding.image_view_unsigned; + // Check for 3D texture sampled as 2D. + bool force_special_view = + (dimension == xenos::FetchOpDimension::k2D && + binding->key.dimension == xenos::DataDimension::k3D); + + if (force_special_view) { + // Get the appropriate texture for signed/unsigned. + Texture* texture = nullptr; + if (is_signed && IsSignedVersionSeparateForFormat(binding->key)) { + texture = binding->texture_signed; + } else { + texture = binding->texture; + } + if (texture) { + image_view = + static_cast(texture)->GetOrCreate3DAs2DImageView( + is_signed, binding->host_swizzle); + } + } else { + const VulkanTextureBinding& vulkan_binding = + vulkan_texture_bindings_[fetch_constant_index]; + image_view = is_signed ? vulkan_binding.image_view_signed + : vulkan_binding.image_view_unsigned; + } } if (image_view != VK_NULL_HANDLE) { return image_view; @@ -1081,7 +1101,8 @@ std::unique_ptr VulkanTextureCache::CreateTexture( VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; // For scaled resolve textures with mips, we need transfer source to generate // mip levels via blit from the base level. - if (key.scaled_resolve && key.mip_max_level > 0) { + // For 3D textures, we need transfer source to support 3D-to-2D conversion. + if ((key.scaled_resolve && key.mip_max_level > 0) || is_3d) { image_create_info.usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT; } image_create_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; @@ -1765,8 +1786,10 @@ void VulkanTextureCache::UpdateTextureBindingsImpl( VulkanTextureCache::VulkanTexture::VulkanTexture( VulkanTextureCache& texture_cache, const TextureKey& key, VkImage image, - VmaAllocation allocation) - : Texture(texture_cache, key), image_(image), allocation_(allocation) { + VmaAllocation allocation, bool track_usage) + : Texture(texture_cache, key, track_usage), + image_(image), + allocation_(allocation) { VmaAllocationInfo allocation_info; vmaGetAllocationInfo(texture_cache.vma_allocator_, allocation_, &allocation_info); @@ -1783,6 +1806,15 @@ VulkanTextureCache::VulkanTexture::~VulkanTexture() { for (const auto& view_pair : views_) { dfn.vkDestroyImageView(device, view_pair.second, nullptr); } + // Clean up 3D-as-2D image views. The texture_3d_as_2d_ wrapper will clean + // itself up via unique_ptr destructor. + if (image_view_3d_as_2d_unsigned_ != VK_NULL_HANDLE) { + dfn.vkDestroyImageView(device, image_view_3d_as_2d_unsigned_, nullptr); + } + if (image_view_3d_as_2d_signed_ != VK_NULL_HANDLE) { + dfn.vkDestroyImageView(device, image_view_3d_as_2d_signed_, nullptr); + } + // texture_3d_as_2d_ is a unique_ptr and will destroy its image/allocation. vmaDestroyImage(vulkan_texture_cache.vma_allocator_, image_, allocation_); } @@ -1877,6 +1909,235 @@ VkImageView VulkanTextureCache::VulkanTexture::GetView(bool is_signed, return view; } +VkImageView VulkanTextureCache::VulkanTexture::GetOrCreate3DAs2DImageView( + bool is_signed, uint32_t host_swizzle) { + int32_t mode = cvars::gpu_3d_to_2d_texture_mode; + + // Mode 0: Feature disabled. + if (mode == 0) { + return VK_NULL_HANDLE; + } + + // Return cached view if available. + VkImageView& cached_view = + is_signed ? image_view_3d_as_2d_signed_ : image_view_3d_as_2d_unsigned_; + if (cached_view != VK_NULL_HANDLE) { + return cached_view; + } + + VulkanTextureCache& vulkan_texture_cache = + static_cast(texture_cache()); + const ui::vulkan::VulkanDevice* const vulkan_device = + vulkan_texture_cache.command_processor_.GetVulkanDevice(); + const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions(); + const VkDevice device = vulkan_device->device(); + + // Create the 2D texture wrapper if it doesn't exist. + if (!texture_3d_as_2d_) { + const HostFormatPair& host_format_pair = + vulkan_texture_cache.GetHostFormatPair(key()); + VkFormat format = host_format_pair.format_unsigned.format; + if (format == VK_FORMAT_UNDEFINED) { + return VK_NULL_HANDLE; + } + + VkImageCreateInfo image_create_info = {}; + image_create_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + image_create_info.imageType = VK_IMAGE_TYPE_2D; + image_create_info.format = format; + image_create_info.extent.width = key().GetWidth(); + image_create_info.extent.height = key().GetHeight(); + image_create_info.extent.depth = 1; + image_create_info.mipLevels = 1; + image_create_info.arrayLayers = 1; + image_create_info.samples = VK_SAMPLE_COUNT_1_BIT; + image_create_info.tiling = VK_IMAGE_TILING_OPTIMAL; + image_create_info.usage = + VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + image_create_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + image_create_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + VmaAllocationCreateInfo allocation_create_info = {}; + allocation_create_info.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE; + + VkImage image_2d; + VmaAllocation allocation_2d; + if (vmaCreateImage(vulkan_texture_cache.vma_allocator_, &image_create_info, + &allocation_create_info, &image_2d, &allocation_2d, + nullptr) != VK_SUCCESS) { + XELOGE("VulkanTexture: Failed to create 3D-as-2D image"); + return VK_NULL_HANDLE; + } + + // Create a modified key for the 2D wrapper with depth=1 and + // mip_max_level=0. Keep dimension as k3D so that in Mode 2, LoadTextureData + // uses 3D tiling math to correctly read slice 0 from the 3D-tiled guest + // memory. + TextureKey key_2d = key(); + key_2d.depth_or_array_size_minus_1 = 0; + key_2d.mip_max_level = 0; + + if (mode == 1) { + // Mode 1: GPU copy - copy slice 0 from the 3D image to the 2D image. + DeferredCommandBuffer& command_buffer = + vulkan_texture_cache.command_processor_.deferred_command_buffer(); + + // Get the current layout from usage tracking (like D3D12 does). + VkPipelineStageFlags src_stage_mask; + VkAccessFlags src_access_mask; + VkImageLayout src_old_layout; + vulkan_texture_cache.GetTextureUsageMasks( + usage_, src_stage_mask, src_access_mask, src_old_layout); + + // Transition 2D image to transfer destination. + VkImageMemoryBarrier barrier_2d_to_dst = {}; + barrier_2d_to_dst.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier_2d_to_dst.srcAccessMask = 0; + barrier_2d_to_dst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier_2d_to_dst.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier_2d_to_dst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier_2d_to_dst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier_2d_to_dst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier_2d_to_dst.image = image_2d; + barrier_2d_to_dst.subresourceRange = + ui::vulkan::util::InitializeSubresourceRange(); + command_buffer.CmdVkPipelineBarrier( + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, + 0, nullptr, 0, nullptr, 1, &barrier_2d_to_dst); + + // Transition 3D image to transfer source. + VkImageMemoryBarrier barrier_3d_to_src = {}; + barrier_3d_to_src.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier_3d_to_src.srcAccessMask = src_access_mask; + barrier_3d_to_src.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier_3d_to_src.oldLayout = src_old_layout; + barrier_3d_to_src.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + barrier_3d_to_src.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier_3d_to_src.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier_3d_to_src.image = image_; + barrier_3d_to_src.subresourceRange = + ui::vulkan::util::InitializeSubresourceRange(); + command_buffer.CmdVkPipelineBarrier( + src_stage_mask, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, + nullptr, 1, &barrier_3d_to_src); + + // Use vkCmdCopyImage for the slice copy. + // This works for all formats including compressed (BC) formats. + VkImageCopy copy_region = {}; + copy_region.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + copy_region.srcSubresource.mipLevel = 0; + copy_region.srcSubresource.baseArrayLayer = 0; + copy_region.srcSubresource.layerCount = 1; + copy_region.srcOffset = {0, 0, 0}; + copy_region.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + copy_region.dstSubresource.mipLevel = 0; + copy_region.dstSubresource.baseArrayLayer = 0; + copy_region.dstSubresource.layerCount = 1; + copy_region.dstOffset = {0, 0, 0}; + copy_region.extent.width = key().GetWidth(); + copy_region.extent.height = key().GetHeight(); + copy_region.extent.depth = 1; + command_buffer.CmdVkCopyImage( + image_, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image_2d, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©_region); + + // Transition 3D image back to guest shader sampled state. + VkPipelineStageFlags dst_stage_mask; + VkAccessFlags dst_access_mask; + VkImageLayout new_layout; + vulkan_texture_cache.GetTextureUsageMasks(Usage::kGuestShaderSampled, + dst_stage_mask, dst_access_mask, + new_layout); + barrier_3d_to_src.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier_3d_to_src.dstAccessMask = dst_access_mask; + barrier_3d_to_src.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + barrier_3d_to_src.newLayout = new_layout; + command_buffer.CmdVkPipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, + dst_stage_mask, 0, 0, nullptr, 0, + nullptr, 1, &barrier_3d_to_src); + // Update tracking - texture is now in guest shader sampled state. + SetUsage(Usage::kGuestShaderSampled); + + // Transition 2D image to shader read. + barrier_2d_to_dst.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier_2d_to_dst.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + barrier_2d_to_dst.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier_2d_to_dst.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + command_buffer.CmdVkPipelineBarrier( + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &barrier_2d_to_dst); + + // Create the wrapper after successful GPU copy. + texture_3d_as_2d_.reset(new VulkanTexture( + vulkan_texture_cache, key_2d, image_2d, allocation_2d, false)); + texture_3d_as_2d_->SetUsage(Usage::kGuestShaderSampled); + } else { + // Mode 2: CPU re-upload - reload texture data from guest memory. + // Create the wrapper first so LoadTextureData can work with it. + texture_3d_as_2d_.reset(new VulkanTexture( + vulkan_texture_cache, key_2d, image_2d, allocation_2d, false)); + + if (!vulkan_texture_cache.LoadTextureData(*texture_3d_as_2d_)) { + XELOGE("VulkanTexture: Failed to untile 3D-as-2D data"); + texture_3d_as_2d_.reset(); + return VK_NULL_HANDLE; + } + + // LoadTextureData leaves the texture in TRANSFER_DST state. + // Transition to shader read state. + DeferredCommandBuffer& command_buffer = + vulkan_texture_cache.command_processor_.deferred_command_buffer(); + VkImageMemoryBarrier barrier_to_shader = {}; + barrier_to_shader.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier_to_shader.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier_to_shader.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + barrier_to_shader.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier_to_shader.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier_to_shader.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier_to_shader.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier_to_shader.image = texture_3d_as_2d_->image(); + barrier_to_shader.subresourceRange = + ui::vulkan::util::InitializeSubresourceRange(); + command_buffer.CmdVkPipelineBarrier( + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &barrier_to_shader); + texture_3d_as_2d_->SetUsage(Usage::kGuestShaderSampled); + } + } + + // Create the image view. + const HostFormatPair& host_format_pair = + vulkan_texture_cache.GetHostFormatPair(key()); + VkFormat format = (is_signed ? host_format_pair.format_signed + : host_format_pair.format_unsigned) + .format; + if (format == VK_FORMAT_UNDEFINED) { + return VK_NULL_HANDLE; + } + + VkImageViewCreateInfo view_create_info = {}; + view_create_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + view_create_info.image = texture_3d_as_2d_->image(); + // Use 2D_ARRAY to match shader expectations (Dim = 2D, Arrayed = 1). + view_create_info.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY; + view_create_info.format = format; + view_create_info.components.r = GetComponentSwizzle(host_swizzle, 0); + view_create_info.components.g = GetComponentSwizzle(host_swizzle, 1); + view_create_info.components.b = GetComponentSwizzle(host_swizzle, 2); + view_create_info.components.a = GetComponentSwizzle(host_swizzle, 3); + view_create_info.subresourceRange = + ui::vulkan::util::InitializeSubresourceRange(); + view_create_info.subresourceRange.layerCount = 1; + + if (dfn.vkCreateImageView(device, &view_create_info, nullptr, &cached_view) != + VK_SUCCESS) { + XELOGE("VulkanTexture: Failed to create 3D-as-2D image view"); + return VK_NULL_HANDLE; + } + + return cached_view; +} + VulkanTextureCache::VulkanTextureCache( const RegisterFile& register_file, VulkanSharedMemory& shared_memory, uint32_t draw_resolution_scale_x, uint32_t draw_resolution_scale_y, diff --git a/src/xenia/gpu/vulkan/vulkan_texture_cache.h b/src/xenia/gpu/vulkan/vulkan_texture_cache.h index 7e8f5525c..00319ddbf 100644 --- a/src/xenia/gpu/vulkan/vulkan_texture_cache.h +++ b/src/xenia/gpu/vulkan/vulkan_texture_cache.h @@ -92,7 +92,7 @@ class VulkanTextureCache final : public TextureCache { VkImageView GetActiveBindingOrNullImageView(uint32_t fetch_constant_index, xenos::FetchOpDimension dimension, - bool is_signed) const; + bool is_signed); SamplerParameters GetSamplerParameters( const VulkanShader::SamplerBinding& binding) const; @@ -254,9 +254,10 @@ class VulkanTextureCache final : public TextureCache { }; // Takes ownership of the image and its memory. + // track_usage: if false, texture won't participate in LRU cache eviction. explicit VulkanTexture(VulkanTextureCache& texture_cache, const TextureKey& key, VkImage image, - VmaAllocation allocation); + VmaAllocation allocation, bool track_usage = true); ~VulkanTexture(); VkImage image() const { return image_; } @@ -271,6 +272,10 @@ class VulkanTextureCache final : public TextureCache { VkImageView GetView(bool is_signed, uint32_t host_swizzle, bool is_array = true); + // For 3D textures sampled as 2D - creates a 2D copy of slice 0. + VkImageView GetOrCreate3DAs2DImageView(bool is_signed, + uint32_t host_swizzle); + private: union ViewKey { uint32_t key; @@ -331,6 +336,14 @@ class VulkanTextureCache final : public TextureCache { Usage usage_ = Usage::kUndefined; std::unordered_map views_; + + // For 3D textures sampled as 2D - cached 2D copy of slice 0. + // This is a wrapper around the 2D image with a modified key (depth=1). + // For Mode 1 (GPU copy), the wrapper is created after the copy. + // For Mode 2 (CPU re-upload), LoadTextureData is called on the wrapper. + std::unique_ptr texture_3d_as_2d_; + VkImageView image_view_3d_as_2d_unsigned_ = VK_NULL_HANDLE; + VkImageView image_view_3d_as_2d_signed_ = VK_NULL_HANDLE; }; struct VulkanTextureBinding { @@ -359,7 +372,8 @@ class VulkanTextureCache final : public TextureCache { case xenos::FetchOpDimension::k1D: case xenos::FetchOpDimension::k2D: return resource_dimension == xenos::DataDimension::k1D || - resource_dimension == xenos::DataDimension::k2DOrStacked; + resource_dimension == xenos::DataDimension::k2DOrStacked || + resource_dimension == xenos::DataDimension::k3D; case xenos::FetchOpDimension::k3DOrStacked: return resource_dimension == xenos::DataDimension::k3D; case xenos::FetchOpDimension::kCube: diff --git a/src/xenia/ui/vulkan/functions/device_1_0.inc b/src/xenia/ui/vulkan/functions/device_1_0.inc index c2c251131..b0c36538c 100644 --- a/src/xenia/ui/vulkan/functions/device_1_0.inc +++ b/src/xenia/ui/vulkan/functions/device_1_0.inc @@ -15,6 +15,7 @@ XE_UI_VULKAN_FUNCTION(vkCmdClearAttachments) XE_UI_VULKAN_FUNCTION(vkCmdClearColorImage) XE_UI_VULKAN_FUNCTION(vkCmdCopyBuffer) XE_UI_VULKAN_FUNCTION(vkCmdCopyBufferToImage) +XE_UI_VULKAN_FUNCTION(vkCmdCopyImage) XE_UI_VULKAN_FUNCTION(vkCmdCopyImageToBuffer) XE_UI_VULKAN_FUNCTION(vkCmdDispatch) XE_UI_VULKAN_FUNCTION(vkCmdDraw) From e7941b0c7d0537408d82bed9b335c60256c3d7c4 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Wed, 14 Jan 2026 00:24:25 +0900 Subject: [PATCH 18/21] [GPU] 3d-to-2d textures default enable Replaced the modal cvar with bool gpu_3d_to_2d_texture with "mode 2" now enabled by default and "mode 1" removed as unnecessary --- src/xenia/gpu/d3d12/d3d12_texture_cache.cc | 79 ++------ src/xenia/gpu/gpu_flags.cc | 9 +- src/xenia/gpu/gpu_flags.h | 2 +- src/xenia/gpu/texture_cache.h | 9 + .../gpu/vulkan/deferred_command_buffer.cc | 10 - .../gpu/vulkan/deferred_command_buffer.h | 37 ---- src/xenia/gpu/vulkan/vulkan_texture_cache.cc | 182 +++++------------- src/xenia/gpu/vulkan/vulkan_texture_cache.h | 6 +- src/xenia/ui/vulkan/functions/device_1_0.inc | 1 - 9 files changed, 78 insertions(+), 257 deletions(-) diff --git a/src/xenia/gpu/d3d12/d3d12_texture_cache.cc b/src/xenia/gpu/d3d12/d3d12_texture_cache.cc index 7a788d641..9c37c483f 100644 --- a/src/xenia/gpu/d3d12/d3d12_texture_cache.cc +++ b/src/xenia/gpu/d3d12/d3d12_texture_cache.cc @@ -1408,7 +1408,11 @@ bool D3D12TextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture, const texture_util::TextureGuestLayout& guest_layout = d3d12_texture.guest_layout(); xenos::DataDimension dimension = texture_key.dimension; + // Whether the host texture is 3D (determines depth vs array layer layout). bool is_3d = dimension == xenos::DataDimension::k3D; + // Whether to use 3D tiling when reading from guest memory. + // For 3D-as-2D wrappers, the host texture is 2D but we need 3D tiling. + bool is_3d_tiling = is_3d || d3d12_texture.force_load_3d_tiling(); uint32_t width = texture_key.GetWidth(); uint32_t height = texture_key.GetHeight(); uint32_t depth_or_array_size = texture_key.GetDepthOrArraySize(); @@ -1597,7 +1601,7 @@ bool D3D12TextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture, assert_true(texture_resolution_scale_x <= 7); assert_true(texture_resolution_scale_y <= 7); load_constants.is_tiled_3d_endian_scale = - uint32_t(texture_key.tiled) | (uint32_t(is_3d) << 1) | + uint32_t(texture_key.tiled) | (uint32_t(is_3d_tiling) << 1) | (uint32_t(texture_key.endianness) << 2) | (texture_resolution_scale_x << 4) | (texture_resolution_scale_y << 7); @@ -1836,10 +1840,7 @@ void D3D12TextureCache::UpdateTextureBindingsImpl( ID3D12Resource* D3D12TextureCache::D3D12Texture::GetOrCreate3DAs2DResource( D3D12_RESOURCE_STATES end_state) { - int32_t mode = cvars::gpu_3d_to_2d_texture_mode; - - // Mode 0: Feature disabled. - if (mode == 0) { + if (!cvars::gpu_3d_to_2d_texture) { return nullptr; } @@ -1877,64 +1878,20 @@ ID3D12Resource* D3D12TextureCache::D3D12Texture::GetOrCreate3DAs2DResource( return nullptr; } - if (mode == 1) { - // Mode 1: GPU copy - copy slice 0 from the 3D texture to the 2D resource. - D3D12_RESOURCE_STATES old_main_state = - SetResourceState(D3D12_RESOURCE_STATE_COPY_SOURCE); - d3d12_cache.command_processor_.PushTransitionBarrier( - resource(), old_main_state, D3D12_RESOURCE_STATE_COPY_SOURCE); + // Create a modified key for the 2D wrapper with depth=1 and mip_max_level=0. + // Keep dimension as k3D so guest layout uses 3D tiling math to correctly + // read slice 0 from the 3D-tiled guest memory. + TextureKey key_2d = key(); + key_2d.depth_or_array_size_minus_1 = 0; + key_2d.mip_max_level = 0; - auto& command_list = - d3d12_cache.command_processor_.GetDeferredCommandList(); + texture_3d_as_2d_.reset(new D3D12Texture( + d3d12_cache, key_2d, resource_2d.Get(), initial_state, false)); - D3D12_TEXTURE_COPY_LOCATION dest_loc = {}; - dest_loc.pResource = resource_2d.Get(); - dest_loc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; - dest_loc.SubresourceIndex = 0; - - D3D12_TEXTURE_COPY_LOCATION src_loc = {}; - src_loc.pResource = resource(); - src_loc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; - src_loc.SubresourceIndex = 0; - - D3D12_BOX src_box; - src_box.left = 0; - src_box.top = 0; - src_box.front = 0; - src_box.right = static_cast(source_desc.Width); - src_box.bottom = source_desc.Height; - src_box.back = 1; // Only copy 1 depth slice. - - command_list.D3DCopyTextureRegion(&dest_loc, 0, 0, 0, &src_loc, &src_box); - - // Restore source state. - d3d12_cache.command_processor_.PushTransitionBarrier( - resource(), D3D12_RESOURCE_STATE_COPY_SOURCE, old_main_state); - SetResourceState(old_main_state); - - // Create a minimal wrapper for the 2D resource. - TextureKey key_2d = key(); - key_2d.depth_or_array_size_minus_1 = 0; - key_2d.mip_max_level = 0; - texture_3d_as_2d_.reset(new D3D12Texture( - d3d12_cache, key_2d, resource_2d.Get(), initial_state, false)); - } else { - // Mode 2: CPU re-upload - reload texture data from guest memory. - // Keep dimension as k3D so LoadTextureData uses 3D tiling math to correctly - // read slice 0 from the 3D-tiled guest memory. - TextureKey key_load = key(); - key_load.depth_or_array_size_minus_1 = 0; - key_load.mip_max_level = 0; - - std::unique_ptr texture_wrap(new D3D12Texture( - d3d12_cache, key_load, resource_2d.Get(), initial_state, false)); - - if (!d3d12_cache.LoadTextureData(*texture_wrap)) { - XELOGE("D3D12Texture: Failed to untile 3D-as-2D data"); - return nullptr; - } - - texture_3d_as_2d_ = std::move(texture_wrap); + if (!d3d12_cache.LoadTextureData(*texture_3d_as_2d_)) { + XELOGE("D3D12Texture: Failed to load 3D-as-2D texture data"); + texture_3d_as_2d_.reset(); + return nullptr; } // Transition to requested state. diff --git a/src/xenia/gpu/gpu_flags.cc b/src/xenia/gpu/gpu_flags.cc index 21634dec8..6d1e063c8 100644 --- a/src/xenia/gpu/gpu_flags.cc +++ b/src/xenia/gpu/gpu_flags.cc @@ -90,8 +90,7 @@ DEFINE_bool(no_discard_stencil_in_transfer_pipelines, false, "May improve performance on some GPUs.", "GPU"); -DEFINE_int32(gpu_3d_to_2d_texture_mode, 2, - "Handle shaders that sample 3D textures as 2D by creating a 2D " - "copy of slice 0. 0 = disabled, 1 = GPU copy, " - "2 = CPU re-upload from guest memory (default).", - "GPU"); +DEFINE_bool(gpu_3d_to_2d_texture, true, + "Handle shaders that sample 3D textures as 2D by creating a 2D " + "texture from slice 0 of the guest memory.", + "GPU"); diff --git a/src/xenia/gpu/gpu_flags.h b/src/xenia/gpu/gpu_flags.h index e73854223..2e865abf5 100644 --- a/src/xenia/gpu/gpu_flags.h +++ b/src/xenia/gpu/gpu_flags.h @@ -36,7 +36,7 @@ DECLARE_bool(disassemble_pm4); DECLARE_bool(no_discard_stencil_in_transfer_pipelines); -DECLARE_int32(gpu_3d_to_2d_texture_mode); +DECLARE_bool(gpu_3d_to_2d_texture); #define XE_GPU_FINE_GRAINED_DRAW_SCOPES 1 diff --git a/src/xenia/gpu/texture_cache.h b/src/xenia/gpu/texture_cache.h index a8d1729dd..f14d14a3d 100644 --- a/src/xenia/gpu/texture_cache.h +++ b/src/xenia/gpu/texture_cache.h @@ -251,6 +251,11 @@ class TextureCache { return guest_layout().mips_total_extent_bytes; } + // For 3D-as-2D wrappers: the host texture is 2D but we need 3D tiling + // when loading from guest memory. + bool force_load_3d_tiling() const { return force_load_3d_tiling_; } + void SetForceLoad3DTiling(bool force) { force_load_3d_tiling_ = force; } + uint64_t GetHostMemoryUsage() const { return host_memory_usage_; } uint64_t last_usage_submission_index() const { @@ -323,6 +328,10 @@ class TextureCache { // Set to false via constructor for wrapper textures. bool in_usage_list_; + // For 3D-as-2D wrappers: use 3D tiling when loading even though the host + // texture is 2D. + bool force_load_3d_tiling_ = false; + // Whether the most up-to-date base / mips contain pages with data from a // resolve operation (rather than from the CPU or memexport), primarily for // choosing between piecewise linear gamma and sRGB when the former is diff --git a/src/xenia/gpu/vulkan/deferred_command_buffer.cc b/src/xenia/gpu/vulkan/deferred_command_buffer.cc index b533b625d..0ddfef765 100644 --- a/src/xenia/gpu/vulkan/deferred_command_buffer.cc +++ b/src/xenia/gpu/vulkan/deferred_command_buffer.cc @@ -175,16 +175,6 @@ void DeferredCommandBuffer::Execute(VkCommandBuffer command_buffer) { args.filter); } break; - case Command::kVkCopyImage: { - auto& args = *reinterpret_cast(stream); - dfn.vkCmdCopyImage( - command_buffer, args.src_image, args.src_image_layout, - args.dst_image, args.dst_image_layout, args.region_count, - reinterpret_cast( - reinterpret_cast(stream) + - xe::align(sizeof(ArgsVkCopyImage), alignof(VkImageCopy)))); - } break; - case Command::kVkDispatch: { auto& args = *reinterpret_cast(stream); dfn.vkCmdDispatch(command_buffer, args.group_count_x, diff --git a/src/xenia/gpu/vulkan/deferred_command_buffer.h b/src/xenia/gpu/vulkan/deferred_command_buffer.h index 8352e06d4..4fef1ee6f 100644 --- a/src/xenia/gpu/vulkan/deferred_command_buffer.h +++ b/src/xenia/gpu/vulkan/deferred_command_buffer.h @@ -260,32 +260,6 @@ class DeferredCommandBuffer { regions, sizeof(VkImageBlit) * region_count); } - VkImageCopy* CmdCopyImageEmplace(VkImage src_image, - VkImageLayout src_image_layout, - VkImage dst_image, - VkImageLayout dst_image_layout, - uint32_t region_count) { - const size_t header_size = - xe::align(sizeof(ArgsVkCopyImage), alignof(VkImageCopy)); - uint8_t* args_ptr = reinterpret_cast( - WriteCommand(Command::kVkCopyImage, - header_size + sizeof(VkImageCopy) * region_count)); - auto& args = *reinterpret_cast(args_ptr); - args.src_image = src_image; - args.src_image_layout = src_image_layout; - args.dst_image = dst_image; - args.dst_image_layout = dst_image_layout; - args.region_count = region_count; - return reinterpret_cast(args_ptr + header_size); - } - void CmdVkCopyImage(VkImage src_image, VkImageLayout src_image_layout, - VkImage dst_image, VkImageLayout dst_image_layout, - uint32_t region_count, const VkImageCopy* regions) { - std::memcpy(CmdCopyImageEmplace(src_image, src_image_layout, dst_image, - dst_image_layout, region_count), - regions, sizeof(VkImageCopy) * region_count); - } - void CmdVkDispatch(uint32_t group_count_x, uint32_t group_count_y, uint32_t group_count_z) { auto& args = *reinterpret_cast( @@ -424,7 +398,6 @@ class DeferredCommandBuffer { kVkCopyBuffer, kVkCopyBufferToImage, kVkBlitImage, - kVkCopyImage, kVkDispatch, kVkDraw, kVkDrawIndexed, @@ -531,16 +504,6 @@ class DeferredCommandBuffer { static_assert(alignof(VkImageBlit) <= alignof(uintmax_t)); }; - struct ArgsVkCopyImage { - VkImage src_image; - VkImageLayout src_image_layout; - VkImage dst_image; - VkImageLayout dst_image_layout; - uint32_t region_count; - // Followed by aligned VkImageCopy[]. - static_assert(alignof(VkImageCopy) <= alignof(uintmax_t)); - }; - struct ArgsVkDispatch { uint32_t group_count_x; uint32_t group_count_y; diff --git a/src/xenia/gpu/vulkan/vulkan_texture_cache.cc b/src/xenia/gpu/vulkan/vulkan_texture_cache.cc index 79d581f90..cc1b5b4f8 100644 --- a/src/xenia/gpu/vulkan/vulkan_texture_cache.cc +++ b/src/xenia/gpu/vulkan/vulkan_texture_cache.cc @@ -1101,8 +1101,7 @@ std::unique_ptr VulkanTextureCache::CreateTexture( VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; // For scaled resolve textures with mips, we need transfer source to generate // mip levels via blit from the base level. - // For 3D textures, we need transfer source to support 3D-to-2D conversion. - if ((key.scaled_resolve && key.mip_max_level > 0) || is_3d) { + if (key.scaled_resolve && key.mip_max_level > 0) { image_create_info.usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT; } image_create_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; @@ -1170,7 +1169,11 @@ bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture, const texture_util::TextureGuestLayout& guest_layout = vulkan_texture.guest_layout(); xenos::DataDimension dimension = texture_key.dimension; + // Whether the host image is 3D (determines depth vs array layer layout). bool is_3d = dimension == xenos::DataDimension::k3D; + // Whether to use 3D tiling when reading from guest memory. + // For 3D-as-2D wrappers, the host image is 2D but we need 3D tiling. + bool is_3d_tiling = is_3d || vulkan_texture.force_load_3d_tiling(); uint32_t width = texture_key.GetWidth(); uint32_t height = texture_key.GetHeight(); uint32_t depth_or_array_size = texture_key.GetDepthOrArraySize(); @@ -1476,7 +1479,7 @@ bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture, assert_true(texture_resolution_scale_x <= 7); assert_true(texture_resolution_scale_y <= 7); load_constants.is_tiled_3d_endian_scale = - uint32_t(texture_key.tiled) | (uint32_t(is_3d) << 1) | + uint32_t(texture_key.tiled) | (uint32_t(is_3d_tiling) << 1) | (uint32_t(texture_key.endianness) << 2) | (texture_resolution_scale_x << 4) | (texture_resolution_scale_y << 7); @@ -1911,10 +1914,7 @@ VkImageView VulkanTextureCache::VulkanTexture::GetView(bool is_signed, VkImageView VulkanTextureCache::VulkanTexture::GetOrCreate3DAs2DImageView( bool is_signed, uint32_t host_swizzle) { - int32_t mode = cvars::gpu_3d_to_2d_texture_mode; - - // Mode 0: Feature disabled. - if (mode == 0) { + if (!cvars::gpu_3d_to_2d_texture) { return VK_NULL_HANDLE; } @@ -1927,10 +1927,6 @@ VkImageView VulkanTextureCache::VulkanTexture::GetOrCreate3DAs2DImageView( VulkanTextureCache& vulkan_texture_cache = static_cast(texture_cache()); - const ui::vulkan::VulkanDevice* const vulkan_device = - vulkan_texture_cache.command_processor_.GetVulkanDevice(); - const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions(); - const VkDevice device = vulkan_device->device(); // Create the 2D texture wrapper if it doesn't exist. if (!texture_3d_as_2d_) { @@ -1970,142 +1966,49 @@ VkImageView VulkanTextureCache::VulkanTexture::GetOrCreate3DAs2DImageView( } // Create a modified key for the 2D wrapper with depth=1 and - // mip_max_level=0. Keep dimension as k3D so that in Mode 2, LoadTextureData - // uses 3D tiling math to correctly read slice 0 from the 3D-tiled guest - // memory. + // mip_max_level=0. Keep dimension as k3D so guest layout uses 3D tiling + // math to correctly read slice 0 from the 3D-tiled guest memory. TextureKey key_2d = key(); key_2d.depth_or_array_size_minus_1 = 0; key_2d.mip_max_level = 0; - if (mode == 1) { - // Mode 1: GPU copy - copy slice 0 from the 3D image to the 2D image. - DeferredCommandBuffer& command_buffer = - vulkan_texture_cache.command_processor_.deferred_command_buffer(); + // Create the wrapper first so LoadTextureData can work with it. + texture_3d_as_2d_.reset(new VulkanTexture(vulkan_texture_cache, key_2d, + image_2d, allocation_2d, false)); - // Get the current layout from usage tracking (like D3D12 does). - VkPipelineStageFlags src_stage_mask; - VkAccessFlags src_access_mask; - VkImageLayout src_old_layout; - vulkan_texture_cache.GetTextureUsageMasks( - usage_, src_stage_mask, src_access_mask, src_old_layout); - - // Transition 2D image to transfer destination. - VkImageMemoryBarrier barrier_2d_to_dst = {}; - barrier_2d_to_dst.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - barrier_2d_to_dst.srcAccessMask = 0; - barrier_2d_to_dst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - barrier_2d_to_dst.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; - barrier_2d_to_dst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - barrier_2d_to_dst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier_2d_to_dst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier_2d_to_dst.image = image_2d; - barrier_2d_to_dst.subresourceRange = - ui::vulkan::util::InitializeSubresourceRange(); - command_buffer.CmdVkPipelineBarrier( - VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, - 0, nullptr, 0, nullptr, 1, &barrier_2d_to_dst); - - // Transition 3D image to transfer source. - VkImageMemoryBarrier barrier_3d_to_src = {}; - barrier_3d_to_src.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - barrier_3d_to_src.srcAccessMask = src_access_mask; - barrier_3d_to_src.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; - barrier_3d_to_src.oldLayout = src_old_layout; - barrier_3d_to_src.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; - barrier_3d_to_src.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier_3d_to_src.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier_3d_to_src.image = image_; - barrier_3d_to_src.subresourceRange = - ui::vulkan::util::InitializeSubresourceRange(); - command_buffer.CmdVkPipelineBarrier( - src_stage_mask, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, - nullptr, 1, &barrier_3d_to_src); - - // Use vkCmdCopyImage for the slice copy. - // This works for all formats including compressed (BC) formats. - VkImageCopy copy_region = {}; - copy_region.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - copy_region.srcSubresource.mipLevel = 0; - copy_region.srcSubresource.baseArrayLayer = 0; - copy_region.srcSubresource.layerCount = 1; - copy_region.srcOffset = {0, 0, 0}; - copy_region.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - copy_region.dstSubresource.mipLevel = 0; - copy_region.dstSubresource.baseArrayLayer = 0; - copy_region.dstSubresource.layerCount = 1; - copy_region.dstOffset = {0, 0, 0}; - copy_region.extent.width = key().GetWidth(); - copy_region.extent.height = key().GetHeight(); - copy_region.extent.depth = 1; - command_buffer.CmdVkCopyImage( - image_, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image_2d, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©_region); - - // Transition 3D image back to guest shader sampled state. - VkPipelineStageFlags dst_stage_mask; - VkAccessFlags dst_access_mask; - VkImageLayout new_layout; - vulkan_texture_cache.GetTextureUsageMasks(Usage::kGuestShaderSampled, - dst_stage_mask, dst_access_mask, - new_layout); - barrier_3d_to_src.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; - barrier_3d_to_src.dstAccessMask = dst_access_mask; - barrier_3d_to_src.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; - barrier_3d_to_src.newLayout = new_layout; - command_buffer.CmdVkPipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, - dst_stage_mask, 0, 0, nullptr, 0, - nullptr, 1, &barrier_3d_to_src); - // Update tracking - texture is now in guest shader sampled state. - SetUsage(Usage::kGuestShaderSampled); - - // Transition 2D image to shader read. - barrier_2d_to_dst.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - barrier_2d_to_dst.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - barrier_2d_to_dst.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - barrier_2d_to_dst.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - command_buffer.CmdVkPipelineBarrier( - VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, - 0, 0, nullptr, 0, nullptr, 1, &barrier_2d_to_dst); - - // Create the wrapper after successful GPU copy. - texture_3d_as_2d_.reset(new VulkanTexture( - vulkan_texture_cache, key_2d, image_2d, allocation_2d, false)); - texture_3d_as_2d_->SetUsage(Usage::kGuestShaderSampled); - } else { - // Mode 2: CPU re-upload - reload texture data from guest memory. - // Create the wrapper first so LoadTextureData can work with it. - texture_3d_as_2d_.reset(new VulkanTexture( - vulkan_texture_cache, key_2d, image_2d, allocation_2d, false)); - - if (!vulkan_texture_cache.LoadTextureData(*texture_3d_as_2d_)) { - XELOGE("VulkanTexture: Failed to untile 3D-as-2D data"); - texture_3d_as_2d_.reset(); - return VK_NULL_HANDLE; - } - - // LoadTextureData leaves the texture in TRANSFER_DST state. - // Transition to shader read state. - DeferredCommandBuffer& command_buffer = - vulkan_texture_cache.command_processor_.deferred_command_buffer(); - VkImageMemoryBarrier barrier_to_shader = {}; - barrier_to_shader.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - barrier_to_shader.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - barrier_to_shader.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - barrier_to_shader.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - barrier_to_shader.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - barrier_to_shader.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier_to_shader.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier_to_shader.image = texture_3d_as_2d_->image(); - barrier_to_shader.subresourceRange = - ui::vulkan::util::InitializeSubresourceRange(); - command_buffer.CmdVkPipelineBarrier( - VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, - 0, 0, nullptr, 0, nullptr, 1, &barrier_to_shader); - texture_3d_as_2d_->SetUsage(Usage::kGuestShaderSampled); + if (!vulkan_texture_cache.LoadTextureData(*texture_3d_as_2d_)) { + XELOGE("VulkanTexture: Failed to load 3D-as-2D texture data"); + texture_3d_as_2d_.reset(); + return VK_NULL_HANDLE; } + + // LoadTextureData leaves the texture in TRANSFER_DST state. + // Transition to shader read state. + DeferredCommandBuffer& command_buffer = + vulkan_texture_cache.command_processor_.deferred_command_buffer(); + VkImageMemoryBarrier barrier_to_shader = {}; + barrier_to_shader.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier_to_shader.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier_to_shader.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + barrier_to_shader.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier_to_shader.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier_to_shader.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier_to_shader.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier_to_shader.image = texture_3d_as_2d_->image(); + barrier_to_shader.subresourceRange = + ui::vulkan::util::InitializeSubresourceRange(); + command_buffer.CmdVkPipelineBarrier( + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &barrier_to_shader); + texture_3d_as_2d_->SetUsage(Usage::kGuestShaderSampled); } // Create the image view. + const ui::vulkan::VulkanDevice* const vulkan_device = + vulkan_texture_cache.command_processor_.GetVulkanDevice(); + const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions(); + const VkDevice device = vulkan_device->device(); + const HostFormatPair& host_format_pair = vulkan_texture_cache.GetHostFormatPair(key()); VkFormat format = (is_signed ? host_format_pair.format_signed @@ -3062,6 +2965,9 @@ void VulkanTextureCache::GetTextureUsageMasks(VulkanTexture::Usage usage, layout = VK_IMAGE_LAYOUT_UNDEFINED; switch (usage) { case VulkanTexture::Usage::kUndefined: + // For UNDEFINED layout, use TOP_OF_PIPE as source stage (wait for + // nothing) with no access mask (discarding old contents). + stage_mask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; break; case VulkanTexture::Usage::kTransferDestination: stage_mask = VK_PIPELINE_STAGE_TRANSFER_BIT; diff --git a/src/xenia/gpu/vulkan/vulkan_texture_cache.h b/src/xenia/gpu/vulkan/vulkan_texture_cache.h index 00319ddbf..ea77d8c1e 100644 --- a/src/xenia/gpu/vulkan/vulkan_texture_cache.h +++ b/src/xenia/gpu/vulkan/vulkan_texture_cache.h @@ -337,10 +337,8 @@ class VulkanTextureCache final : public TextureCache { std::unordered_map views_; - // For 3D textures sampled as 2D - cached 2D copy of slice 0. - // This is a wrapper around the 2D image with a modified key (depth=1). - // For Mode 1 (GPU copy), the wrapper is created after the copy. - // For Mode 2 (CPU re-upload), LoadTextureData is called on the wrapper. + // For 3D textures sampled as 2D - cached 2D texture loaded from slice 0. + // Uses a modified key (depth=1) with 3D tiling to read from guest memory. std::unique_ptr texture_3d_as_2d_; VkImageView image_view_3d_as_2d_unsigned_ = VK_NULL_HANDLE; VkImageView image_view_3d_as_2d_signed_ = VK_NULL_HANDLE; diff --git a/src/xenia/ui/vulkan/functions/device_1_0.inc b/src/xenia/ui/vulkan/functions/device_1_0.inc index b0c36538c..c2c251131 100644 --- a/src/xenia/ui/vulkan/functions/device_1_0.inc +++ b/src/xenia/ui/vulkan/functions/device_1_0.inc @@ -15,7 +15,6 @@ XE_UI_VULKAN_FUNCTION(vkCmdClearAttachments) XE_UI_VULKAN_FUNCTION(vkCmdClearColorImage) XE_UI_VULKAN_FUNCTION(vkCmdCopyBuffer) XE_UI_VULKAN_FUNCTION(vkCmdCopyBufferToImage) -XE_UI_VULKAN_FUNCTION(vkCmdCopyImage) XE_UI_VULKAN_FUNCTION(vkCmdCopyImageToBuffer) XE_UI_VULKAN_FUNCTION(vkCmdDispatch) XE_UI_VULKAN_FUNCTION(vkCmdDraw) From 0e2967cdb0b3577e83b822ab75a8518705ae1d02 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Wed, 25 Feb 2026 13:28:04 +0900 Subject: [PATCH 19/21] [D3D12] Fix 3D-to-2D texture crash under Wine/VKD3D --- src/xenia/gpu/d3d12/d3d12_texture_cache.cc | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/xenia/gpu/d3d12/d3d12_texture_cache.cc b/src/xenia/gpu/d3d12/d3d12_texture_cache.cc index 9c37c483f..aabd464a8 100644 --- a/src/xenia/gpu/d3d12/d3d12_texture_cache.cc +++ b/src/xenia/gpu/d3d12/d3d12_texture_cache.cc @@ -1858,12 +1858,21 @@ ID3D12Resource* D3D12TextureCache::D3D12Texture::GetOrCreate3DAs2DResource( d3d12_cache.command_processor_.GetD3D12Provider(); ID3D12Device* device = provider.GetDevice(); - D3D12_RESOURCE_DESC source_desc = resource_->GetDesc(); - D3D12_RESOURCE_DESC desc = source_desc; + // Build the 2D resource desc from scratch rather than copying from the 3D + // resource's GetDesc(), as inherited internal state can cause issues with + // VKD3D (D3D12 over Vulkan translation used by Wine). + D3D12_RESOURCE_DESC desc = {}; desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + desc.Width = key().GetWidth(); + desc.Height = key().GetHeight(); desc.DepthOrArraySize = 1; desc.MipLevels = 1; - desc.Alignment = 0; + desc.Format = d3d12_cache.GetDXGIResourceFormat(key()); + if (desc.Format == DXGI_FORMAT_UNKNOWN) { + return nullptr; + } + desc.SampleDesc.Count = 1; + desc.SampleDesc.Quality = 0; desc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; desc.Flags = D3D12_RESOURCE_FLAG_NONE; From 0c8d1b763fb09e93338006c21a61c4e196877fe2 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:50:43 +0900 Subject: [PATCH 20/21] [D3D12] Pre-create 3D-as-2D textures before draw setup --- src/xenia/gpu/d3d12/d3d12_texture_cache.cc | 27 ++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/xenia/gpu/d3d12/d3d12_texture_cache.cc b/src/xenia/gpu/d3d12/d3d12_texture_cache.cc index aabd464a8..2a7315d5f 100644 --- a/src/xenia/gpu/d3d12/d3d12_texture_cache.cc +++ b/src/xenia/gpu/d3d12/d3d12_texture_cache.cc @@ -483,6 +483,33 @@ void D3D12TextureCache::RequestTextures(uint32_t used_texture_mask) { TextureCache::RequestTextures(used_texture_mask); + // Pre-create 3D-as-2D wrappers for any 3D textures while we're still in + // the texture loading phase (before graphics pipeline setup). LoadTextureData + // dispatches compute shaders, which must not happen during draw call setup + // as VKD3D asserts a graphics pipeline is active at that point. + if (cvars::gpu_3d_to_2d_texture) { + uint32_t textures_3d = used_texture_mask; + uint32_t index_3d; + while (xe::bit_scan_forward(textures_3d, &index_3d)) { + textures_3d = xe::clear_lowest_bit(textures_3d); + const TextureBinding* binding = GetValidTextureBinding(index_3d); + if (!binding || binding->key.dimension != xenos::DataDimension::k3D) { + continue; + } + D3D12Texture* texture = static_cast(binding->texture); + if (texture) { + texture->GetOrCreate3DAs2DResource( + D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + } + D3D12Texture* texture_signed = + static_cast(binding->texture_signed); + if (texture_signed) { + texture_signed->GetOrCreate3DAs2DResource( + D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + } + } + } + // Transition the textures to the needed usage - always in // NON_PIXEL_SHADER_RESOURCE | PIXEL_SHADER_RESOURCE states because barriers // between read-only stages, if needed, are discouraged (also if these were From 125b4c8c05851e48faa73fda2a593f0629c5f7df Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Sat, 17 Jan 2026 22:41:48 +0900 Subject: [PATCH 21/21] [GPU] Ensure force_special_view checks swizzle signs before using signed This was causing unnecessary logspam in Blue Dragon due to incorrectly trying to request signed when the fetch constants are unsigned --- src/xenia/gpu/d3d12/d3d12_texture_cache.cc | 5 ++++- src/xenia/gpu/vulkan/vulkan_texture_cache.cc | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/xenia/gpu/d3d12/d3d12_texture_cache.cc b/src/xenia/gpu/d3d12/d3d12_texture_cache.cc index 2a7315d5f..18ba18476 100644 --- a/src/xenia/gpu/d3d12/d3d12_texture_cache.cc +++ b/src/xenia/gpu/d3d12/d3d12_texture_cache.cc @@ -708,8 +708,11 @@ uint32_t D3D12TextureCache::GetActiveTextureBindlessSRVIndex( if (force_special_view) { // Determine which texture object to use + // Respect swizzled_signs from fetch constant, not just shader request Texture* texture = nullptr; - if (host_shader_binding.is_signed) { + bool use_signed = host_shader_binding.is_signed && + texture_util::IsAnySignSigned(binding->swizzled_signs); + if (use_signed) { texture = IsSignedVersionSeparateForFormat(binding->key) ? binding->texture_signed : binding->texture; diff --git a/src/xenia/gpu/vulkan/vulkan_texture_cache.cc b/src/xenia/gpu/vulkan/vulkan_texture_cache.cc index cc1b5b4f8..155358546 100644 --- a/src/xenia/gpu/vulkan/vulkan_texture_cache.cc +++ b/src/xenia/gpu/vulkan/vulkan_texture_cache.cc @@ -616,8 +616,11 @@ VkImageView VulkanTextureCache::GetActiveBindingOrNullImageView( if (force_special_view) { // Get the appropriate texture for signed/unsigned. + // Respect swizzled_signs from fetch constant, not just shader request. Texture* texture = nullptr; - if (is_signed && IsSignedVersionSeparateForFormat(binding->key)) { + bool use_signed = + is_signed && texture_util::IsAnySignSigned(binding->swizzled_signs); + if (use_signed && IsSignedVersionSeparateForFormat(binding->key)) { texture = binding->texture_signed; } else { texture = binding->texture; @@ -625,7 +628,7 @@ VkImageView VulkanTextureCache::GetActiveBindingOrNullImageView( if (texture) { image_view = static_cast(texture)->GetOrCreate3DAs2DImageView( - is_signed, binding->host_swizzle); + use_signed, binding->host_swizzle); } } else { const VulkanTextureBinding& vulkan_binding =