mirror of
https://github.com/xenia-project/xenia.git
synced 2026-07-11 01:24:34 +02:00
cec9ca0ef2
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.
752 lines
34 KiB
C++
752 lines
34 KiB
C++
/**
|
|
******************************************************************************
|
|
* Xenia : Xbox 360 Emulator Research Project *
|
|
******************************************************************************
|
|
* Copyright 2022 Ben Vanik. All rights reserved. *
|
|
* Released under the BSD license - see LICENSE in the root for more details. *
|
|
******************************************************************************
|
|
*/
|
|
|
|
#ifndef XENIA_GPU_RENDER_TARGET_CACHE_H_
|
|
#define XENIA_GPU_RENDER_TARGET_CACHE_H_
|
|
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <functional>
|
|
#include <map>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "third_party/fmt/include/fmt/format.h"
|
|
#include "xenia/base/assert.h"
|
|
#include "xenia/base/cvar.h"
|
|
#include "xenia/gpu/draw_extent_estimator.h"
|
|
#include "xenia/gpu/draw_util.h"
|
|
#include "xenia/gpu/register_file.h"
|
|
#include "xenia/gpu/registers.h"
|
|
#include "xenia/gpu/shader.h"
|
|
#include "xenia/gpu/xenos.h"
|
|
|
|
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_unorm16);
|
|
DECLARE_bool(native_2x_msaa);
|
|
DECLARE_bool(native_stencil_value_output);
|
|
DECLARE_bool(snorm16_render_target_full_range);
|
|
|
|
namespace xe {
|
|
namespace gpu {
|
|
|
|
class RenderTargetCache {
|
|
public:
|
|
// High-level emulation logic implementation path.
|
|
enum class Path {
|
|
// Approximate method using conventional host render targets and copying
|
|
// ("transferring ownership" of tiles) between render targets to support
|
|
// aliasing.
|
|
//
|
|
// May be irreparably inaccurate, completely at the mercy of the host API's
|
|
// fixed-function output-merger, primarily because it has to perform
|
|
// blending - and when using a different pixel format, it will behave
|
|
// differently (the most important factor here is the range - it's clamped
|
|
// for normalized formats, but not for floating-point ones).
|
|
//
|
|
// On a Direct3D 11-level device, formats which can be mapped directly
|
|
// (disregarding things like blending internal precision details):
|
|
// - 8_8_8_8
|
|
// - 2_10_10_10
|
|
// - 32_FLOAT
|
|
// - 32_32_FLOAT
|
|
// - D24S8
|
|
// Can be mapped directly, but require handling in shaders:
|
|
// - D24FS8 with truncated SV_DepthLessEqual output (or SV_Depth, which is
|
|
// suboptimal, as it prevents early depth / stencil from working). To
|
|
// support bit-exact reinterpretation to and from D24F for unmodified
|
|
// areas using pixel shader depth output without unrestricted depth range,
|
|
// 0...1 of the guest depth should be mapped to 0...0.5 on the host in the
|
|
// viewport and conversion.
|
|
// Can be mapped directly, but not supporting rare edge cases:
|
|
// - 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 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
|
|
// truncate the range for blending to work correctly, or divide by 32 in
|
|
// shaders breaking multiplication in blending.
|
|
kHostRenderTargets,
|
|
|
|
// Custom output-merger implementation, with full per-pixel and per-sample
|
|
// control, however, only available on hosts with raster-ordered writes from
|
|
// pixel shaders.
|
|
kPixelShaderInterlock,
|
|
};
|
|
|
|
// Pixel shader interlock implementation helpers.
|
|
|
|
// Appended to the format in the format constant via bitwise OR.
|
|
enum : uint32_t {
|
|
kPSIColorFormatFlag_64bpp_Shift = xenos::kColorRenderTargetFormatBits,
|
|
// Requires clamping of blending sources and factors.
|
|
kPSIColorFormatFlag_FixedPointColor_Shift,
|
|
kPSIColorFormatFlag_FixedPointAlpha_Shift,
|
|
|
|
kPSIColorFormatFlag_64bpp = uint32_t(1) << kPSIColorFormatFlag_64bpp_Shift,
|
|
kPSIColorFormatFlag_FixedPointColor =
|
|
uint32_t(1) << kPSIColorFormatFlag_FixedPointColor_Shift,
|
|
kPSIColorFormatFlag_FixedPointAlpha =
|
|
uint32_t(1) << kPSIColorFormatFlag_FixedPointAlpha_Shift,
|
|
};
|
|
|
|
static constexpr uint32_t AddPSIColorFormatFlags(
|
|
xenos::ColorRenderTargetFormat format) {
|
|
uint32_t format_flags = uint32_t(format);
|
|
if (format == xenos::ColorRenderTargetFormat::k_16_16_16_16 ||
|
|
format == xenos::ColorRenderTargetFormat::k_16_16_16_16_FLOAT ||
|
|
format == xenos::ColorRenderTargetFormat::k_32_32_FLOAT) {
|
|
format_flags |= kPSIColorFormatFlag_64bpp;
|
|
}
|
|
if (format == xenos::ColorRenderTargetFormat::k_8_8_8_8 ||
|
|
format == xenos::ColorRenderTargetFormat::k_8_8_8_8_GAMMA ||
|
|
format == xenos::ColorRenderTargetFormat::k_2_10_10_10 ||
|
|
format == xenos::ColorRenderTargetFormat::k_16_16 ||
|
|
format == xenos::ColorRenderTargetFormat::k_16_16_16_16 ||
|
|
format == xenos::ColorRenderTargetFormat::k_2_10_10_10_AS_10_10_10_10) {
|
|
format_flags |= kPSIColorFormatFlag_FixedPointColor |
|
|
kPSIColorFormatFlag_FixedPointAlpha;
|
|
} else if (format == xenos::ColorRenderTargetFormat::k_2_10_10_10_FLOAT ||
|
|
format == xenos::ColorRenderTargetFormat::
|
|
k_2_10_10_10_FLOAT_AS_16_16_16_16) {
|
|
format_flags |= kPSIColorFormatFlag_FixedPointAlpha;
|
|
}
|
|
return format_flags;
|
|
}
|
|
|
|
static void GetPSIColorFormatInfo(xenos::ColorRenderTargetFormat format,
|
|
uint32_t write_mask, float& clamp_rgb_low,
|
|
float& clamp_alpha_low,
|
|
float& clamp_rgb_high,
|
|
float& clamp_alpha_high,
|
|
uint32_t& keep_mask_low,
|
|
uint32_t& keep_mask_high);
|
|
|
|
virtual ~RenderTargetCache();
|
|
|
|
virtual Path GetPath() const = 0;
|
|
|
|
// Resolution scaling on the EDRAM side is performed by multiplying the EDRAM
|
|
// tile size by the resolution scale.
|
|
// Note: Only integer scaling factors are provided because fractional ones,
|
|
// even with 0.5 granularity, cause significant issues in addition to the ones
|
|
// already present with integer scaling. 1.5 (from 1280x720 to 1920x1080) may
|
|
// be useful, but it would cause pixel coverage issues with odd dimensions of
|
|
// screen-space geometry, most importantly 1x1 that is often the final step in
|
|
// reduction algorithms such as average luminance computation in HDR. A
|
|
// single-pixel quad, either 0...1 without half-pixel offset or 0.5...1.5 with
|
|
// it (covers only the first pixel according the top-left rule), with 1.5x
|
|
// resolution scaling, would become 0...1.5 (only the first pixel covered) or
|
|
// 0.75...2.25 (only the second). The workaround used in Xenia for 2x and 3x
|
|
// resolution scaling for filling the gap caused by the half-pixel offset
|
|
// becoming whole-pixel - stretching the second column / row of pixels into
|
|
// the first - will not work in this case, as for one-pixel primitives without
|
|
// half-pixel offset (covering only the first pixel, but not the second, with
|
|
// 1.5x), it will actually cause the pixel to be erased with 1.5x scaling. As
|
|
// within one pass there can be geometry both with and without the half-pixel
|
|
// offset (not only depending on PA_SU_VTX_CNTL::PIX_CENTER, but also with the
|
|
// half-pixel offset possibly reverted manually), the emulator can't decide
|
|
// whether the stretching workaround actually needs to be used. So, with 1.5x,
|
|
// depending on how the game draws its screen-space effects and on whether the
|
|
// workaround is used, in some cases, nothing will just be drawn to the first
|
|
// pixel, while in other cases, the effect will be drawn to it, but the
|
|
// stretching workaround will replace it with the undefined value in the
|
|
// second pixel. Also, with 1.5x, rounding of integer coordinates becomes
|
|
// complicated, also in part due to the half-pixel offset. Odd texture sizes
|
|
// would need to be rounded down, as according to the top-left rule, a 1.5x1.5
|
|
// quad at the 0 or 0.75 origin (after the scaling) will cover only 1 pixel -
|
|
// so, if the resulting texture was 2x2 rather than 1x1, undefined pixels
|
|
// would participate in filtering. However, 1x1 scissor rounded to 1x1, with
|
|
// the half-pixel offset of vertices, would cause the entire 0.75...2.25 quad
|
|
// to be discarded.
|
|
uint32_t draw_resolution_scale_x() const { return draw_resolution_scale_x_; }
|
|
uint32_t draw_resolution_scale_y() const { return draw_resolution_scale_y_; }
|
|
bool IsDrawResolutionScaled() const {
|
|
return draw_resolution_scale_x() > 1 || draw_resolution_scale_y() > 1;
|
|
}
|
|
|
|
// Virtual (both the common code and the implementation may do something
|
|
// here), don't call from destructors (does work not needed for shutdown
|
|
// also).
|
|
virtual void ClearCache();
|
|
|
|
virtual void BeginFrame();
|
|
|
|
virtual bool Update(bool is_rasterization_done,
|
|
reg::RB_DEPTHCONTROL normalized_depth_control,
|
|
uint32_t normalized_color_mask,
|
|
const Shader& vertex_shader);
|
|
|
|
// Returns bits where 0 is whether a depth render target is currently bound on
|
|
// the host and 1... are whether the same applies to color render targets, and
|
|
// formats (resource formats, but if needed, with gamma taken into account) of
|
|
// each.
|
|
uint32_t GetLastUpdateBoundRenderTargets(
|
|
uint32_t* depth_and_color_formats_out = nullptr) const;
|
|
|
|
protected:
|
|
RenderTargetCache(const RegisterFile& register_file, const Memory& memory,
|
|
TraceWriter* trace_writer, uint32_t draw_resolution_scale_x,
|
|
uint32_t draw_resolution_scale_y)
|
|
: register_file_(register_file),
|
|
draw_extent_estimator_(register_file, memory, trace_writer),
|
|
draw_resolution_scale_x_(draw_resolution_scale_x),
|
|
draw_resolution_scale_y_(draw_resolution_scale_y) {
|
|
assert_not_zero(draw_resolution_scale_x);
|
|
assert_not_zero(draw_resolution_scale_y);
|
|
}
|
|
|
|
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();
|
|
// May be called from the destructor, or from the implementation shutdown to
|
|
// destroy all render targets before destroying what they depend on in the
|
|
// implementation.
|
|
void DestroyAllRenderTargets(bool shutting_down);
|
|
// Call last in implementation-specific shutdown, also callable from the
|
|
// destructor.
|
|
void ShutdownCommon();
|
|
|
|
// For host render targets, implemented via transfer of ownership of EDRAM
|
|
// 80x16-sample tiles between host render targets. When a range is
|
|
// transferred, its data is copied, bit-exactly from the guest's perspective
|
|
// (when dangerous, such as because of non-propagated NaN, primarily in the
|
|
// float16 case, by drawing to an integer view of the render target texture),
|
|
// from the previous host render target to the new one, by drawing rectangles
|
|
// with a pixel shader converting the previous host render target to a guest
|
|
// bit pattern, reinterpreting it in the new format. If depth is emulated with
|
|
// float32, this may lead to loss of data - specifically for depth, both guest
|
|
// format ownership and float32 ownership are tracked, and to let color data
|
|
// overwrite depth data, loading during ownership transfer is done from
|
|
// intersections of the current guest ownership ranges and float32 ownership
|
|
// ranges. Ownership transfer happens when a render target is needed - based
|
|
// on the current viewport; or, if no viewport is available, ownership of the
|
|
// rest of the EDRAM is transferred.
|
|
|
|
union RenderTargetKey {
|
|
uint32_t key;
|
|
struct {
|
|
uint32_t base_tiles : xenos::kEdramBaseTilesBits; // 11
|
|
// At 4x MSAA (2 horizontal samples), max. align(8192 * 2, 80) / 80 = 205.
|
|
// For pitch at 64bpp, multiply by 2 (or use GetPitchTiles).
|
|
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.
|
|
uint32_t resource_format : xenos::kRenderTargetFormatBits; // 26
|
|
};
|
|
|
|
RenderTargetKey() : key(0) { static_assert_size(*this, sizeof(key)); }
|
|
|
|
struct Hasher {
|
|
size_t operator()(const RenderTargetKey& render_target_key) const {
|
|
return std::hash<uint32_t>{}(render_target_key.key);
|
|
}
|
|
};
|
|
bool operator==(const RenderTargetKey& other_key) const {
|
|
return key == other_key.key;
|
|
}
|
|
bool operator!=(const RenderTargetKey& other_key) const {
|
|
return !(*this == other_key);
|
|
}
|
|
|
|
bool IsEmpty() const {
|
|
// Meaningless when pitch_tiles_at_32bpp == 0, but for comparison
|
|
// purposes, only treat everything being 0 as a special case.
|
|
return key == 0;
|
|
}
|
|
|
|
xenos::ColorRenderTargetFormat GetColorFormat() const {
|
|
assert_false(is_depth);
|
|
return xenos::ColorRenderTargetFormat(resource_format);
|
|
}
|
|
xenos::DepthRenderTargetFormat GetDepthFormat() const {
|
|
assert_true(is_depth);
|
|
return xenos::DepthRenderTargetFormat(resource_format);
|
|
}
|
|
bool Is64bpp() const {
|
|
if (is_depth) {
|
|
return false;
|
|
}
|
|
return xenos::IsColorRenderTargetFormat64bpp(GetColorFormat());
|
|
}
|
|
const char* GetFormatName() const {
|
|
return is_depth ? xenos::GetDepthRenderTargetFormatName(GetDepthFormat())
|
|
: xenos::GetColorRenderTargetFormatName(GetColorFormat());
|
|
}
|
|
|
|
uint32_t GetPitchTiles() const {
|
|
return pitch_tiles_at_32bpp << uint32_t(Is64bpp());
|
|
}
|
|
static constexpr uint32_t GetWidth(uint32_t pitch_tiles_at_32bpp,
|
|
xenos::MsaaSamples msaa_samples) {
|
|
return pitch_tiles_at_32bpp *
|
|
(xenos::kEdramTileWidthSamples >>
|
|
uint32_t(msaa_samples >= xenos::MsaaSamples::k4X));
|
|
}
|
|
uint32_t GetWidth() const {
|
|
return GetWidth(pitch_tiles_at_32bpp, msaa_samples);
|
|
}
|
|
|
|
std::string GetDebugName() const {
|
|
return fmt::format("RT @ {}t, <{}t>, {}xMSAA, {}", base_tiles,
|
|
GetPitchTiles(), uint32_t(1) << uint32_t(msaa_samples),
|
|
GetFormatName());
|
|
}
|
|
};
|
|
|
|
class RenderTarget {
|
|
public:
|
|
virtual ~RenderTarget() = default;
|
|
// Exclusive ownership, plus no point in moving (only allocated via new).
|
|
RenderTarget(const RenderTarget& render_target) = delete;
|
|
RenderTarget& operator=(const RenderTarget& render_target) = delete;
|
|
RenderTarget(RenderTarget&& render_target) = delete;
|
|
RenderTarget& operator=(RenderTarget&& render_target) = delete;
|
|
RenderTargetKey key() const { return key_; }
|
|
|
|
protected:
|
|
RenderTarget(RenderTargetKey key) : key_(key) {}
|
|
|
|
private:
|
|
RenderTargetKey key_;
|
|
};
|
|
|
|
struct Transfer {
|
|
uint32_t start_tiles;
|
|
uint32_t end_tiles;
|
|
RenderTarget* source;
|
|
RenderTarget* host_depth_source;
|
|
Transfer(uint32_t start_tiles, uint32_t end_tiles, RenderTarget* source,
|
|
RenderTarget* host_depth_source)
|
|
: start_tiles(start_tiles),
|
|
end_tiles(end_tiles),
|
|
source(source),
|
|
host_depth_source(host_depth_source) {
|
|
assert_true(start_tiles < end_tiles);
|
|
}
|
|
struct Rectangle {
|
|
uint32_t x_pixels;
|
|
uint32_t y_pixels;
|
|
uint32_t width_pixels;
|
|
uint32_t height_pixels;
|
|
};
|
|
static constexpr uint32_t kMaxRectanglesWithoutCutout = 3;
|
|
static constexpr uint32_t kMaxCutoutBorderRectangles = 4;
|
|
static constexpr uint32_t kMaxRectanglesWithCutout =
|
|
kMaxRectanglesWithoutCutout * kMaxCutoutBorderRectangles;
|
|
// Cutout can be specified for resolve clears - not to transfer areas that
|
|
// will be cleared to a single value anyway.
|
|
static uint32_t GetRangeRectangles(uint32_t start_tiles, uint32_t end_tiles,
|
|
uint32_t base_tiles,
|
|
uint32_t pitch_tiles,
|
|
xenos::MsaaSamples msaa_samples,
|
|
bool is_64bpp, Rectangle* rectangles_out,
|
|
const Rectangle* cutout = nullptr);
|
|
uint32_t GetRectangles(uint32_t base_tiles, uint32_t pitch_tiles,
|
|
xenos::MsaaSamples msaa_samples, bool is_64bpp,
|
|
Rectangle* rectangles_out,
|
|
const Rectangle* cutout = nullptr) const {
|
|
return GetRangeRectangles(start_tiles, end_tiles, base_tiles, pitch_tiles,
|
|
msaa_samples, is_64bpp, rectangles_out, cutout);
|
|
}
|
|
bool AreSourcesSame(const Transfer& other_transfer) const {
|
|
return source == other_transfer.source &&
|
|
host_depth_source == other_transfer.host_depth_source;
|
|
}
|
|
|
|
private:
|
|
static uint32_t AddRectangle(const Rectangle& rectangle,
|
|
Rectangle* rectangles_out,
|
|
const Rectangle* cutout = nullptr);
|
|
};
|
|
|
|
union HostDepthStoreRectangleConstant {
|
|
uint32_t constant;
|
|
struct {
|
|
// - 1 because the maximum is 0x1FFF / 8, not 0x2000 / 8.
|
|
uint32_t x_pixels_div_8 : xenos::kResolveSizeBits - 1 -
|
|
xenos::kResolveAlignmentPixelsLog2;
|
|
uint32_t y_pixels_div_8 : xenos::kResolveSizeBits - 1 -
|
|
xenos::kResolveAlignmentPixelsLog2;
|
|
uint32_t width_pixels_div_8_minus_1 : xenos::kResolveSizeBits - 1 -
|
|
xenos::kResolveAlignmentPixelsLog2;
|
|
};
|
|
HostDepthStoreRectangleConstant() : constant(0) {
|
|
static_assert_size(*this, sizeof(constant));
|
|
}
|
|
};
|
|
|
|
union HostDepthStoreRenderTargetConstant {
|
|
uint32_t constant;
|
|
struct {
|
|
uint32_t pitch_tiles : xenos::kEdramPitchTilesBits;
|
|
uint32_t resolution_scale_x : 3;
|
|
uint32_t resolution_scale_y : 3;
|
|
// Whether 2x MSAA is supported natively rather than through 4x.
|
|
uint32_t msaa_2x_supported : 1;
|
|
};
|
|
HostDepthStoreRenderTargetConstant() : constant(0) {
|
|
static_assert_size(*this, sizeof(constant));
|
|
}
|
|
};
|
|
|
|
struct HostDepthStoreConstants {
|
|
HostDepthStoreRectangleConstant rectangle;
|
|
HostDepthStoreRenderTargetConstant render_target;
|
|
};
|
|
|
|
struct ResolveCopyDumpRectangle {
|
|
RenderTarget* render_target;
|
|
// If rows == 1:
|
|
// Row row_first span:
|
|
// [row_first_start, row_last_end)
|
|
// If rows > 1:
|
|
// Row row_first + row span:
|
|
// [row_first_start, row_length_used)
|
|
// Rows [row_first + 1, row_first + rows - 1) span:
|
|
// [row * pitch, row * pitch + row_length_used)
|
|
// Row row_first + rows - 1 span:
|
|
// [row * pitch, row * pitch + row_last_end)
|
|
uint32_t row_first;
|
|
uint32_t rows;
|
|
uint32_t row_first_start;
|
|
uint32_t row_last_end;
|
|
ResolveCopyDumpRectangle(RenderTarget* render_target, uint32_t row_first,
|
|
uint32_t rows, uint32_t row_first_start,
|
|
uint32_t row_last_end)
|
|
: render_target(render_target),
|
|
row_first(row_first),
|
|
rows(rows),
|
|
row_first_start(row_first_start),
|
|
row_last_end(row_last_end) {}
|
|
struct Dispatch {
|
|
// Base plus offset may exceed the EDRAM tile count in case of EDRAM
|
|
// addressing wrapping.
|
|
uint32_t offset;
|
|
uint32_t width_tiles;
|
|
uint32_t height_tiles;
|
|
};
|
|
static constexpr uint32_t kMaxDispatches = 3;
|
|
uint32_t GetDispatches(uint32_t pitch_tiles, uint32_t row_length_used,
|
|
Dispatch* dispatches_out) const {
|
|
if (!rows) {
|
|
return 0;
|
|
}
|
|
// If the first and / or the last rows have the same X spans as the middle
|
|
// part, merge them with it.
|
|
uint32_t dispatch_count = 0;
|
|
if (rows == 1 || row_first_start) {
|
|
Dispatch& dispatch_first = dispatches_out[dispatch_count++];
|
|
dispatch_first.offset = row_first * pitch_tiles + row_first_start;
|
|
dispatch_first.width_tiles =
|
|
(rows == 1 ? row_last_end : row_length_used) - row_first_start;
|
|
dispatch_first.height_tiles = 1;
|
|
if (rows == 1) {
|
|
return dispatch_count;
|
|
}
|
|
}
|
|
uint32_t mid_row_first = row_first + 1;
|
|
uint32_t mid_rows = rows - 2;
|
|
if (!row_first_start) {
|
|
--mid_row_first;
|
|
++mid_rows;
|
|
}
|
|
if (row_last_end == row_length_used) {
|
|
++mid_rows;
|
|
}
|
|
if (mid_rows) {
|
|
Dispatch& dispatch_mid = dispatches_out[dispatch_count++];
|
|
dispatch_mid.offset = mid_row_first * pitch_tiles;
|
|
dispatch_mid.width_tiles = row_length_used;
|
|
dispatch_mid.height_tiles = mid_rows;
|
|
}
|
|
if (row_last_end != row_length_used) {
|
|
Dispatch& dispatch_last = dispatches_out[dispatch_count++];
|
|
dispatch_last.offset = (row_first + rows - 1) * pitch_tiles;
|
|
dispatch_last.width_tiles = row_last_end;
|
|
dispatch_last.height_tiles = 1;
|
|
}
|
|
return dispatch_count;
|
|
}
|
|
};
|
|
|
|
virtual uint32_t GetMaxRenderTargetWidth() const = 0;
|
|
virtual uint32_t GetMaxRenderTargetHeight() const = 0;
|
|
|
|
// Returns the height of a render target that's needed and can be created,
|
|
// taking guest and host limits into account. EDRAM base and 32bpp/64bpp are
|
|
// not taken into account, the same height is used for all render targets even
|
|
// if the implementation supports mixed-size render targets, so the
|
|
// implementation can freely disable individual render targets and let the
|
|
// other ones use the newly available space without restarting the whole
|
|
// render pass (on Vulkan, the actually used height is specified in
|
|
// VkFramebuffer).
|
|
uint32_t GetRenderTargetHeight(uint32_t pitch_tiles_at_32bpp,
|
|
xenos::MsaaSamples msaa_samples) const;
|
|
|
|
virtual RenderTarget* CreateRenderTarget(RenderTargetKey key) = 0;
|
|
|
|
// Whether depth buffer is encoded differently on the host, thus after
|
|
// aliasing naively, precision may be lost - host depth must only be
|
|
// overwritten if the new guest value is different than the current host depth
|
|
// when converted to the guest format (this catches the usual case of
|
|
// overwriting the depth buffer for clearing it mostly). 534507D6 intro
|
|
// cutscene, for example, has a good example of corruption that happens if
|
|
// this is not handled - the upper 1280x384 pixels are rendered in a very
|
|
// "striped" way if the depth precision is lost (if this is made always return
|
|
// false).
|
|
virtual bool IsHostDepthEncodingDifferent(
|
|
xenos::DepthRenderTargetFormat format) const = 0;
|
|
|
|
void ResetAccumulatedRenderTargets() {
|
|
are_accumulated_render_targets_valid_ = false;
|
|
}
|
|
RenderTarget* const* last_update_accumulated_render_targets() const {
|
|
assert_true(GetPath() == Path::kHostRenderTargets);
|
|
return last_update_accumulated_render_targets_;
|
|
}
|
|
|
|
const std::vector<Transfer>* last_update_transfers() const {
|
|
assert_true(GetPath() == Path::kHostRenderTargets);
|
|
return last_update_transfers_;
|
|
}
|
|
|
|
HostDepthStoreRenderTargetConstant GetHostDepthStoreRenderTargetConstant(
|
|
uint32_t pitch_tiles, bool msaa_2x_supported) const {
|
|
HostDepthStoreRenderTargetConstant constant;
|
|
constant.pitch_tiles = pitch_tiles;
|
|
// 3 bits for each.
|
|
assert_true(draw_resolution_scale_x() <= 7);
|
|
assert_true(draw_resolution_scale_y() <= 7);
|
|
constant.resolution_scale_x = draw_resolution_scale_x();
|
|
constant.resolution_scale_y = draw_resolution_scale_y();
|
|
constant.msaa_2x_supported = uint32_t(msaa_2x_supported);
|
|
return constant;
|
|
}
|
|
void GetHostDepthStoreRectangleInfo(
|
|
const Transfer::Rectangle& transfer_rectangle,
|
|
xenos::MsaaSamples msaa_samples,
|
|
HostDepthStoreRectangleConstant& rectangle_constant_out,
|
|
uint32_t& group_count_x_out, uint32_t& group_count_y_out) const;
|
|
|
|
// Returns mappings between ranges within the specified tile rectangle (not
|
|
// render target texture rectangle - textures may have any pitch they need)
|
|
// from ResolveInfo::GetCopyEdramTileSpan and render targets owning them to
|
|
// rectangles_out.
|
|
void GetResolveCopyRectanglesToDump(
|
|
uint32_t base, uint32_t row_length, uint32_t rows, uint32_t pitch,
|
|
std::vector<ResolveCopyDumpRectangle>& rectangles_out) const;
|
|
|
|
// Sets up the needed render targets and transfers to perform a clear in a
|
|
// resolve operation via a host render target clear. resolve_info is expected
|
|
// to be obtained via draw_util::GetResolveInfo. Returns whether any clears
|
|
// need to be done (false in both empty and error cases).
|
|
// TODO(Triang3l): Try to defer clears until the first draw in the next pass
|
|
// (if it uses one or both render targets being cleared) for tile-based GPUs.
|
|
bool PrepareHostRenderTargetsResolveClear(
|
|
const draw_util::ResolveInfo& resolve_info,
|
|
Transfer::Rectangle& clear_rectangle_out,
|
|
RenderTarget*& depth_render_target_out,
|
|
std::vector<Transfer>& depth_transfers_out,
|
|
RenderTarget*& color_render_target_out,
|
|
std::vector<Transfer>& color_transfers_out);
|
|
|
|
// For restoring EDRAM contents from frame traces, obtains or creates a render
|
|
// target at base 0 with of 1280 (only 1 sample and color because copying
|
|
// between MSAA render targets and buffers is not possible in Direct3D 12, and
|
|
// depth may require additional format conversions, not needed really) and
|
|
// transfers ownership of the entire EDRAM to that render target. If a
|
|
// full-EDRAM render target can't be created (for instance, due to size
|
|
// limitations on the host), nullptr is returned.
|
|
RenderTarget* PrepareFullEdram1280xRenderTargetForSnapshotRestoration(
|
|
xenos::ColorRenderTargetFormat color_format);
|
|
|
|
// For pixel shader interlock.
|
|
|
|
virtual void RequestPixelShaderInterlockBarrier() {}
|
|
|
|
// To be called by the implementation when interlocked writes to all of the
|
|
// EDRAM memory are committed with a memory barrier.
|
|
void PixelShaderInterlockFullEdramBarrierPlaced();
|
|
|
|
private:
|
|
const RegisterFile& register_file_;
|
|
uint32_t draw_resolution_scale_x_;
|
|
uint32_t draw_resolution_scale_y_;
|
|
|
|
DrawExtentEstimator draw_extent_estimator_;
|
|
|
|
// For host render targets.
|
|
|
|
struct OwnershipRange {
|
|
uint32_t end_tiles;
|
|
// Need to store keys, not pointers to render targets themselves, because
|
|
// ownership transfer is also what's used to determine when to place
|
|
// barriers with pixel shader interlock, and in this case there are no host
|
|
// render targets.
|
|
// Render target this range is last used by.
|
|
RenderTargetKey render_target;
|
|
// Last host-side depth render targets that used this range even if it has
|
|
// been used by a different render target since then, only used if the
|
|
// respective format has a different encoding on the host. They are tracked
|
|
// separately, overwritten if the host value converted to the guest format
|
|
// becomes out of sync with the guest value. Even if the host uses float32
|
|
// to emulate both unorm24 and float24 (Vulkan on AMD), the unorm24 and
|
|
// float24 render targets are tracked separately from each other, so
|
|
// switching between unorm24 and float24 for the same depth data (clearing
|
|
// of most render targets is done through unorm24 without a viewport - very
|
|
// common) is not destructive as well (f32tof24(host_f32) == guest_f24 does
|
|
// not imply f32tou24(host_f32) == guest_u24, thus aliasing float24 with
|
|
// unorm24 through the same float32 buffer will drop the precision of the
|
|
// float32 value to that of an unorm24 with a totally wrong value). If the
|
|
// range hasn't been used yet (render_target.IsEmpty() == true), these are
|
|
// empty too.
|
|
RenderTargetKey host_depth_render_target_unorm24;
|
|
RenderTargetKey host_depth_render_target_float24;
|
|
OwnershipRange(uint32_t end_tiles, RenderTargetKey render_target,
|
|
RenderTargetKey host_depth_render_target_unorm24,
|
|
RenderTargetKey host_depth_render_target_float24)
|
|
: end_tiles(end_tiles),
|
|
render_target(render_target),
|
|
host_depth_render_target_unorm24(host_depth_render_target_unorm24),
|
|
host_depth_render_target_float24(host_depth_render_target_float24) {}
|
|
const RenderTargetKey& GetHostDepthRenderTarget(
|
|
xenos::DepthRenderTargetFormat resource_format) const {
|
|
assert_true(
|
|
resource_format == xenos::DepthRenderTargetFormat::kD24S8 ||
|
|
resource_format == xenos::DepthRenderTargetFormat::kD24FS8,
|
|
"Illegal resource format");
|
|
return resource_format == xenos::DepthRenderTargetFormat::kD24S8
|
|
? host_depth_render_target_unorm24
|
|
: host_depth_render_target_float24;
|
|
}
|
|
RenderTargetKey& GetHostDepthRenderTarget(
|
|
xenos::DepthRenderTargetFormat resource_format) {
|
|
return const_cast<RenderTargetKey&>(
|
|
const_cast<const OwnershipRange*>(this)->GetHostDepthRenderTarget(
|
|
resource_format));
|
|
}
|
|
bool IsOwnedBy(RenderTargetKey key,
|
|
bool host_depth_encoding_different) const {
|
|
if (render_target != key) {
|
|
// Last time used for something else. If it's a depth render target with
|
|
// different host depth encoding, might have been overwritten by color,
|
|
// or by a depth render target of a different format.
|
|
return false;
|
|
}
|
|
if (host_depth_encoding_different && !key.is_depth &&
|
|
GetHostDepthRenderTarget(key.GetDepthFormat()) != key) {
|
|
// Depth encoding is the same, but different addressing is needed.
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
bool AreOwnersSame(const OwnershipRange& other_range) const {
|
|
return render_target == other_range.render_target &&
|
|
host_depth_render_target_unorm24 ==
|
|
other_range.host_depth_render_target_unorm24 &&
|
|
host_depth_render_target_float24 ==
|
|
other_range.host_depth_render_target_float24;
|
|
}
|
|
};
|
|
|
|
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);
|
|
}
|
|
|
|
RenderTarget* GetOrCreateRenderTarget(RenderTargetKey key);
|
|
|
|
// Checks if changing ownership of the range to the specified render target
|
|
// would require transferring data - primarily for barrier placement on the
|
|
// pixel shader interlock path (where transfers do not involve copying, but
|
|
// barriers are still needed before accessing ranges written before the
|
|
// barrier and addressed by different target-independent rasterization pixel
|
|
// positions.
|
|
bool WouldOwnershipChangeRequireTransfers(RenderTargetKey dest,
|
|
uint32_t start_tiles_base_relative,
|
|
uint32_t length_tiles) const;
|
|
// Updates ownership_ranges_, adds the transfers needed for the ownership
|
|
// change to transfers_append_out if it's not null.
|
|
void ChangeOwnership(
|
|
RenderTargetKey dest, uint32_t start_tiles_base_relative,
|
|
uint32_t length_tiles, std::vector<Transfer>* transfers_append_out,
|
|
const Transfer::Rectangle* resolve_clear_cutout = nullptr);
|
|
|
|
// If failed to create, may contain nullptr to prevent attempting to create a
|
|
// render target twice.
|
|
std::unordered_map<RenderTargetKey, RenderTarget*, RenderTargetKey::Hasher>
|
|
render_targets_;
|
|
|
|
// Map of host render targets currently containing the most up-to-date version
|
|
// of the tile. Has no gaps, unused parts are represented by empty render
|
|
// target keys.
|
|
// TODO(Triang3l): Pool allocator (or a custom red-black tree with one even),
|
|
// since standard containers use dynamic allocation for elements, though
|
|
// changes to this throughout a frame are pretty rare.
|
|
std::map<uint32_t, OwnershipRange> ownership_ranges_;
|
|
|
|
// Render targets actually used by the draw call with the last successful
|
|
// update. 0 is depth, color starting from 1, nullptr if not bound.
|
|
// Only valid for non-pixel-shader-interlock paths.
|
|
RenderTarget*
|
|
last_update_used_render_targets_[1 + xenos::kMaxColorRenderTargets];
|
|
// Render targets used by the draw call with the last successful update or
|
|
// previous updates, unless a different or a totally new one was bound (or
|
|
// surface info was changed), to avoid unneeded render target switching (which
|
|
// is especially undesirable on tile-based GPUs) in the implementation if
|
|
// simply disabling depth / stencil test or color writes and then re-enabling
|
|
// (58410954 does this often with color). Must also be used to determine
|
|
// whether it's safe to enable depth / stencil or writing to a specific color
|
|
// render target in the pipeline for this draw call.
|
|
// Only valid for non-pixel-shader-interlock paths.
|
|
RenderTarget*
|
|
last_update_accumulated_render_targets_[1 +
|
|
xenos::kMaxColorRenderTargets];
|
|
// 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.
|
|
bool are_accumulated_render_targets_valid_ = false;
|
|
// After an update (for simplicity, even an unsuccessful update invalidates
|
|
// this), contains needed ownership transfer sources for each of the current
|
|
// render targets. They are reordered so for one source, all transfers are
|
|
// consecutive in the array.
|
|
std::vector<Transfer>
|
|
last_update_transfers_[1 + xenos::kMaxColorRenderTargets];
|
|
};
|
|
|
|
} // namespace gpu
|
|
} // namespace xe
|
|
|
|
#endif // XENIA_GPU_RENDER_TARGET_CACHE_H_
|