[GPU] 8-bit PWL gamma RT as linear 16-bit UNorm on the host

With render target HLE, directly store linear values as R16G16B16A16_UNORM
without gamma conversion, as this format provides more than enough bits
(need at least 11 per component due to the maximum scale being 2^3 in the
piecewise linear gamma curve) to represent linear values without precision
loss.

This makes blending work correctly in linear space, improving quality of
transparency, lighting passes, and fixing issues such as transparent parts
of impact and footstep decals in 4D5307E6 being bright instead.

The new behavior is enabled by default, as it hugely improves the accuracy
of emulation of this format, that is pretty commonplace in Xbox 360 games,
with likely just a small GPU memory and bandwidth usage increase, compared
to the alternatives that were previously available on the HLE RB path.

It's currently implemented only on Direct3D 12, as most of the current GPU
emulation code is planned to be phased out and redone, and no methods other
than 8-bit with pre-conversion were implemented on Vulkan previously.

To implement on Vulkan later, same conversion as in the Direct3D 12
implementation will need to be done in ownership transfer and resolve
shaders. Currently it's somewhat inconvenient to decouple the conversion
functions in `SpirvShaderTranslator` from an instance of the translator due
to vector constant usage. Later, simpler SPIR-V generation functions may be
added (`spv::Builder` usage in general is overly verbose).

The previously default method (8-bit storage with pre-conversion in shaders
and incorrect blending) can be re-enabled by setting the
"gamma_render_target_as_unorm16" configuration option to `false`. This may
be useful if the game, for instance, switches between 8_8_8_8_GAMMA and
8_8_8_8 formats for the same data frequently, as switching will result in
EDRAM range ownership transfer data copying now. Also, the old path is
preserved for Vulkan devices not supporting R16G16B16A16_UNORM with
blending.

The other workaround that was available previously, replacing the PWL
encoding with host hardware sRGB with linear-space blending in render
target management and in texture fetching, was also inherently inaccurate
in many ways (especially when games have their own PWL encoding math, like
4541080F that displayed incorrect colors on the loading screen), and
required tracking of the encoding needed for ranges in the memory.

The sRGB workaround therefore was deleted in this commit, greatly
simplifying the code in the parts of render target, texture and memory
management and shader generation that were involved in it.
This commit is contained in:
Triang3l
2026-01-17 18:09:42 +03:00
parent f2fabfdf04
commit cec9ca0ef2
34 changed files with 3072 additions and 3188 deletions

View File

@@ -32,7 +32,7 @@ DECLARE_bool(depth_transfer_not_equal_test);
DECLARE_bool(depth_float24_round);
DECLARE_bool(depth_float24_convert_in_pixel_shader);
DECLARE_bool(draw_resolution_scaled_texture_offsets);
DECLARE_bool(gamma_render_target_as_srgb);
DECLARE_bool(gamma_render_target_as_unorm16);
DECLARE_bool(native_2x_msaa);
DECLARE_bool(native_stencil_value_output);
DECLARE_bool(snorm16_render_target_full_range);
@@ -72,12 +72,16 @@ class RenderTargetCache {
// - 16_16_FLOAT, k_16_16_16_16_FLOAT - the Xenos float16 doesn't have
// special values.
// Significant differences:
// - 8_8_8_8_GAMMA - the piecewise linear gamma curve is very different than
// sRGB, one possible path is conversion in shaders (resulting in
// incorrect blending, especially visible on decals in 4D5307E6), another
// is using sRGB render targets and either conversion on resolve or
// reading the resolved data as a true sRGB texture (incorrect when the
// game accesses the data directly, like 4541080F).
// - 8_8_8_8_GAMMA - the piecewise linear gamma precision distribution
// encoding is very different from sRGB. Linear space blending can be
// obtained by promoting to R16G16B16A16_UNORM, but for compact storage,
// conversion in pixel shader output may be done, though it results in
// incorrect blending, especially visible on decals in 4D5307E6. Emulating
// by replacing the encoding with sRGB for render target writes and
// resolved texture reads could work for some games, but certain games,
// such as 4541080F, perform piecewise gamma encoding calculations in
// their code, and that produces noticeably incorrect results if the
// encoding is changed in guest texture memory.
// - 2_10_10_10_FLOAT - ranges significantly different than in float16, much
// smaller RGB range, and alpha is fixed-point and has only 2 bits.
// - 16_16, 16_16_16_16 - has -32 to 32 range, not -1 to 1 - need either to
@@ -91,28 +95,6 @@ class RenderTargetCache {
kPixelShaderInterlock,
};
// Useful host-specific values.
// sRGB conversion from the Direct3D 11.3 functional specification.
static constexpr float kSrgbToLinearDenominator1 = 12.92f;
static constexpr float kSrgbToLinearDenominator2 = 1.055f;
static constexpr float kSrgbToLinearExponent = 2.4f;
static constexpr float kSrgbToLinearOffset = 0.055f;
static constexpr float kSrgbToLinearThreshold = 0.04045f;
static constexpr float SrgbToLinear(float srgb) {
// 0 and 1 must be exactly achievable, also convert NaN to 0.
if (!(srgb > 0.0f)) {
return 0.0f;
}
if (!(srgb < 1.0f)) {
return 1.0f;
}
if (srgb <= kSrgbToLinearThreshold) {
return srgb / kSrgbToLinearDenominator1;
}
return std::pow((srgb + kSrgbToLinearOffset) / kSrgbToLinearDenominator2,
kSrgbToLinearExponent);
}
// Pixel shader interlock implementation helpers.
// Appended to the format in the format constant via bitwise OR.
@@ -221,7 +203,6 @@ class RenderTargetCache {
// formats (resource formats, but if needed, with gamma taken into account) of
// each.
uint32_t GetLastUpdateBoundRenderTargets(
bool distinguish_gamma_formats,
uint32_t* depth_and_color_formats_out = nullptr) const;
protected:
@@ -238,6 +219,8 @@ class RenderTargetCache {
const RegisterFile& register_file() const { return register_file_; }
virtual bool IsGammaFormatHostStorageSeparate() const = 0;
// Call last in implementation-specific initialization (when things like path
// are initialized by the implementation).
void InitializeCommon();
@@ -274,7 +257,7 @@ class RenderTargetCache {
uint32_t pitch_tiles_at_32bpp : 8; // 19
xenos::MsaaSamples msaa_samples : xenos::kMsaaSamplesBits; // 21
uint32_t is_depth : 1; // 22
// Ignoring the blending precision and sRGB.
// Ignoring the blending precision.
uint32_t resource_format : xenos::kRenderTargetFormatBits; // 26
};
@@ -548,10 +531,6 @@ class RenderTargetCache {
assert_true(GetPath() == Path::kHostRenderTargets);
return last_update_accumulated_render_targets_;
}
uint32_t last_update_accumulated_color_targets_are_gamma() const {
assert_true(GetPath() == Path::kHostRenderTargets);
return last_update_accumulated_color_targets_are_gamma_;
}
const std::vector<Transfer>* last_update_transfers() const {
assert_true(GetPath() == Path::kHostRenderTargets);
@@ -697,11 +676,10 @@ class RenderTargetCache {
}
};
static constexpr xenos::ColorRenderTargetFormat GetColorResourceFormat(
xenos::ColorRenderTargetFormat format) {
// sRGB, if used on the host, is a view property or global state - linear
// and sRGB host render targets can share data directly without transfers.
if (format == xenos::ColorRenderTargetFormat::k_8_8_8_8_GAMMA) {
xenos::ColorRenderTargetFormat GetColorResourceFormat(
xenos::ColorRenderTargetFormat format) const {
if (format == xenos::ColorRenderTargetFormat::k_8_8_8_8_GAMMA &&
!IsGammaFormatHostStorageSeparate()) {
return xenos::ColorRenderTargetFormat::k_8_8_8_8;
}
return xenos::GetStorageColorFormat(format);
@@ -755,10 +733,6 @@ class RenderTargetCache {
RenderTarget*
last_update_accumulated_render_targets_[1 +
xenos::kMaxColorRenderTargets];
// Whether the color render targets (in bits 0...3) from the last successful
// update have k_8_8_8_8_GAMMA format, for sRGB emulation on the host if
// needed.
uint32_t last_update_accumulated_color_targets_are_gamma_;
// If false, the next update must copy last_update_used_render_targets_ to
// last_update_accumulated_render_targets_ - it's not beneficial or even
// incorrect to keep the previously bound render targets.