[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
This commit is contained in:
Herman S.
2026-01-08 11:49:05 +09:00
parent 39a65586ce
commit 1fdc786642
6 changed files with 26 additions and 30 deletions
@@ -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);
+8
View File
@@ -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");
+2
View File
@@ -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_
@@ -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;
@@ -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;
@@ -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<uint32_t>(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);