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);