From 1fdc786642cad8cec2a6e85e02eec2d461aa492a Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Thu, 8 Jan 2026 11:46:26 +0900 Subject: [PATCH] [GPU] readback_resolve_half_pixel_offset to control downscale behavior Always defaulting this to true leads to games reading wrong gamma values e.g. dark souls, but it does help in other cases such as image quality for screenshots --- src/xenia/gpu/d3d12/d3d12_command_processor.cc | 9 +++++---- src/xenia/gpu/gpu_flags.cc | 8 ++++++++ src/xenia/gpu/gpu_flags.h | 2 ++ src/xenia/gpu/shaders/resolve_downscale.cs.glsl | 14 +++----------- src/xenia/gpu/shaders/resolve_downscale.cs.hlsl | 14 +++----------- src/xenia/gpu/vulkan/vulkan_command_processor.cc | 9 +++++---- 6 files changed, 26 insertions(+), 30 deletions(-) diff --git a/src/xenia/gpu/d3d12/d3d12_command_processor.cc b/src/xenia/gpu/d3d12/d3d12_command_processor.cc index 67adf0f27..1b5aa6eff 100644 --- a/src/xenia/gpu/d3d12/d3d12_command_processor.cc +++ b/src/xenia/gpu/d3d12/d3d12_command_processor.cc @@ -3576,10 +3576,11 @@ bool D3D12CommandProcessor::IssueCopy_ReadbackResolvePath() { constants.scale_y = scale_y; constants.pixel_size_log2 = pixel_size_log2; constants.tile_count = tile_count; - // Apply half-pixel offset correction when resolution scaling is active. - // Most Xbox 360 games use D3D9-style half-pixel offset, which at Nx - // resolution becomes an N/2 pixel offset that needs correction. - constants.half_pixel_offset = (scale_x > 1 || scale_y > 1) ? 1 : 0; + // Optionally sample from center of scaled block instead of top-left. + constants.half_pixel_offset = (cvars::readback_resolve_half_pixel_offset && + (scale_x > 1 || scale_y > 1)) + ? 1 + : 0; deferred_command_list_.D3DSetComputeRoot32BitConstants( UINT(ResolveDownscaleRootParameter::kConstants), sizeof(constants) / sizeof(uint32_t), &constants, 0); diff --git a/src/xenia/gpu/gpu_flags.cc b/src/xenia/gpu/gpu_flags.cc index d25d64dca..4c0c75e82 100644 --- a/src/xenia/gpu/gpu_flags.cc +++ b/src/xenia/gpu/gpu_flags.cc @@ -171,3 +171,11 @@ DEFINE_bool( "artifacts while pipelines are being created. When disabled, pipelines are " "created synchronously which causes stutter but no visual artifacts.", "GPU"); + +DEFINE_bool( + readback_resolve_half_pixel_offset, false, + "When resolution scaling is active, sample from the center of each scaled " + "pixel block during readback resolve instead of the top-left corner. May " + "improve image quality in some cases but can break games that rely on " + "reading back specific pixel values (e.g., for gamma detection).", + "GPU"); diff --git a/src/xenia/gpu/gpu_flags.h b/src/xenia/gpu/gpu_flags.h index 3fab44c76..9a363d7d6 100644 --- a/src/xenia/gpu/gpu_flags.h +++ b/src/xenia/gpu/gpu_flags.h @@ -56,6 +56,8 @@ DECLARE_bool(no_discard_stencil_in_transfer_pipelines); DECLARE_bool(async_shader_compilation); +DECLARE_bool(readback_resolve_half_pixel_offset); + #define XE_GPU_FINE_GRAINED_DRAW_SCOPES 1 #endif // XENIA_GPU_GPU_FLAGS_H_ diff --git a/src/xenia/gpu/shaders/resolve_downscale.cs.glsl b/src/xenia/gpu/shaders/resolve_downscale.cs.glsl index a3c8c1211..db60aed29 100644 --- a/src/xenia/gpu/shaders/resolve_downscale.cs.glsl +++ b/src/xenia/gpu/shaders/resolve_downscale.cs.glsl @@ -15,8 +15,7 @@ // // By default, picks the top-left pixel of each scale_x * scale_y block. // When xe_downscale_half_pixel_offset is set, samples from (scale/2, scale/2) -// within each block to compensate for the half-pixel offset becoming a -// full-pixel offset at higher resolutions. +// within each block instead. layout(local_size_x = 32, local_size_y = 32, local_size_z = 1) in; @@ -26,11 +25,8 @@ layout(push_constant) uniform ResolveDownscaleConstants { uint xe_downscale_pixel_size_log2; // 0=8bit, 1=16bit, 2=32bit, 3=64bit uint xe_downscale_tile_count; // Number of 32x32 tiles to process uint xe_downscale_source_offset_bytes; // Byte offset into source buffer - // When non-zero, apply half-pixel offset correction by sampling from - // (scale/2, scale/2) within each scaled block instead of (0, 0). - // This compensates for the D3D9-style half-pixel offset used by Xbox 360 - // games, which at Nx resolution scaling shifts rendered content by - // (N/2, N/2) host pixels. + // When non-zero, sample from (scale/2, scale/2) within each scaled block + // instead of (0, 0). uint xe_downscale_half_pixel_offset; }; @@ -65,10 +61,6 @@ void main() { uint tile_size_scaled = tile_size_1x * scale_xy; // Compute offset within each scaled block to sample from. - // Without half-pixel correction: sample from (0, 0) = linear offset 0. - // With half-pixel correction: sample from (scale/2, scale/2) to compensate - // for the D3D9-style half-pixel offset shifting content by (N/2, N/2) pixels - // at Nx resolution. uint block_sample_offset = 0u; if (xe_downscale_half_pixel_offset != 0u && scale_xy > 1u) { uint offset_x = xe_downscale_scale_x >> 1u; diff --git a/src/xenia/gpu/shaders/resolve_downscale.cs.hlsl b/src/xenia/gpu/shaders/resolve_downscale.cs.hlsl index 91c2f95ca..9fbf7d82b 100644 --- a/src/xenia/gpu/shaders/resolve_downscale.cs.hlsl +++ b/src/xenia/gpu/shaders/resolve_downscale.cs.hlsl @@ -13,19 +13,15 @@ // // By default, picks the top-left pixel of each scale_x * scale_y block. // When xe_downscale_half_pixel_offset is set, samples from (scale/2, scale/2) -// within each block to compensate for the half-pixel offset becoming a -// full-pixel offset at higher resolutions. +// within each block instead. cbuffer XeResolveDownscaleConstants : register(b0) { uint xe_downscale_scale_x; // 1 to kMaxDrawResolutionScaleAlongAxis uint xe_downscale_scale_y; // 1 to kMaxDrawResolutionScaleAlongAxis uint xe_downscale_pixel_size_log2; // 0=8bit, 1=16bit, 2=32bit, 3=64bit uint xe_downscale_tile_count; // Number of 32x32 tiles to process - // When non-zero, apply half-pixel offset correction by sampling from - // (scale/2, scale/2) within each scaled block instead of (0, 0). - // This compensates for the D3D9-style half-pixel offset used by Xbox 360 - // games, which at Nx resolution scaling shifts rendered content by - // (N/2, N/2) host pixels. + // When non-zero, sample from (scale/2, scale/2) within each scaled block + // instead of (0, 0). uint xe_downscale_half_pixel_offset; }; @@ -59,10 +55,6 @@ void main(uint3 xe_group_id : SV_GroupID, uint tile_size_scaled = tile_size_1x * scale_xy; // Compute offset within each scaled block to sample from. - // Without half-pixel correction: sample from (0, 0) = linear offset 0. - // With half-pixel correction: sample from (scale/2, scale/2) to compensate - // for the D3D9-style half-pixel offset shifting content by (N/2, N/2) pixels - // at Nx resolution. uint block_sample_offset = 0u; [branch] if (xe_downscale_half_pixel_offset != 0u && scale_xy > 1u) { uint offset_x = xe_downscale_scale_x >> 1u; diff --git a/src/xenia/gpu/vulkan/vulkan_command_processor.cc b/src/xenia/gpu/vulkan/vulkan_command_processor.cc index b32316522..86536eb69 100644 --- a/src/xenia/gpu/vulkan/vulkan_command_processor.cc +++ b/src/xenia/gpu/vulkan/vulkan_command_processor.cc @@ -4514,10 +4514,11 @@ bool VulkanCommandProcessor::IssueCopy() { constants.pixel_size_log2 = pixel_size_log2; constants.tile_count = tile_count; constants.source_offset_bytes = static_cast(source_offset); - // Apply half-pixel offset correction when resolution scaling is active. - // Most Xbox 360 games use D3D9-style half-pixel offset, which at Nx - // resolution becomes an N/2 pixel offset that needs correction. - constants.half_pixel_offset = (scale_x > 1 || scale_y > 1) ? 1 : 0; + // Optionally sample from center of scaled block instead of top-left. + constants.half_pixel_offset = (cvars::readback_resolve_half_pixel_offset && + (scale_x > 1 || scale_y > 1)) + ? 1 + : 0; deferred_command_buffer_.CmdVkPushConstants( resolve_downscale_pipeline_layout_, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(constants), &constants);