GPU: native RHI abstraction (nrhi) + Vulkan backend for the native renderer
This commit is contained in:
parent
065cade4d3
commit
da06b7a51b
16 changed files with 4768 additions and 258 deletions
|
|
@ -43,6 +43,10 @@
|
|||
#include <rex/ui/d3d12/d3d12_upload_buffer_pool.h>
|
||||
#include <rex/ui/d3d12/d3d12_util.h>
|
||||
|
||||
namespace rex::graphics::nrhi {
|
||||
class Device;
|
||||
}
|
||||
|
||||
namespace rex::graphics::d3d12 {
|
||||
|
||||
class D3D12CommandProcessor : public CommandProcessor {
|
||||
|
|
@ -595,6 +599,10 @@ class D3D12CommandProcessor : public CommandProcessor {
|
|||
bool gamma_ramp_256_entry_table_up_to_date_ = false;
|
||||
bool gamma_ramp_pwl_up_to_date_ = false;
|
||||
|
||||
// Native-render RHI device (rex/graphics/native_rhi.h), created lazily on
|
||||
// the first swap with a native guest-output renderer registered.
|
||||
nrhi::Device* native_rhi_device_ = nullptr;
|
||||
|
||||
struct ApplyGammaConstants {
|
||||
uint32_t size[2];
|
||||
};
|
||||
|
|
|
|||
33
include/rex/graphics/d3d12/native_rhi_d3d12.h
Normal file
33
include/rex/graphics/d3d12/native_rhi_d3d12.h
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#pragma once
|
||||
|
||||
// D3D12 implementation of the native-render RHI (rex/graphics/native_rhi.h).
|
||||
// Constructed and owned by the D3D12 command processor; a thin passthrough
|
||||
// onto the deferred command list, the CP barrier queue and the CP submission
|
||||
// counters, reproducing exactly the D3D12 usage the scene renderer was
|
||||
// originally written against.
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include <rex/graphics/native_rhi.h>
|
||||
#include <rex/ui/d3d12/d3d12_api.h>
|
||||
|
||||
namespace rex::graphics::d3d12 {
|
||||
|
||||
class D3D12CommandProcessor;
|
||||
|
||||
// Creates the device wrapper (call once; destroy with
|
||||
// DestroyNativeRhiDevice after all GPU work completed).
|
||||
nrhi::Device* CreateNativeRhiDevice(D3D12CommandProcessor* command_processor);
|
||||
void DestroyNativeRhiDevice(nrhi::Device* device);
|
||||
|
||||
// Per-frame, inside the guest-output refresher: wraps the presenter's guest
|
||||
// output resource (cached by identity), drains completed retirement, and
|
||||
// returns the frame Cmd. guest_output_out receives the wrapped texture.
|
||||
nrhi::Cmd* NativeRhiBeginFrame(nrhi::Device* device,
|
||||
ID3D12Resource* guest_output_resource,
|
||||
DXGI_FORMAT guest_output_format,
|
||||
D3D12_RESOURCE_STATES guest_output_internal_state,
|
||||
uint32_t width, uint32_t height,
|
||||
nrhi::Texture** guest_output_out);
|
||||
|
||||
} // namespace rex::graphics::d3d12
|
||||
|
|
@ -2,13 +2,7 @@
|
|||
|
||||
#include <cstdint>
|
||||
|
||||
#if REX_HAS_D3D12
|
||||
#include <rex/ui/d3d12/d3d12_api.h>
|
||||
#endif
|
||||
|
||||
namespace rex::graphics::d3d12 {
|
||||
class D3D12CommandProcessor;
|
||||
}
|
||||
#include <rex/graphics/native_rhi.h>
|
||||
|
||||
namespace rex::graphics {
|
||||
|
||||
|
|
@ -18,6 +12,10 @@ enum class NativeGuestOutputBackend : uint32_t {
|
|||
kVulkan,
|
||||
};
|
||||
|
||||
// Per-frame context handed to the registered native guest-output renderer.
|
||||
// Backend-agnostic: all device access and command recording goes through the
|
||||
// native-render RHI (rex/graphics/native_rhi.h); the command processor owns
|
||||
// the nrhi::Device and the per-frame nrhi::Cmd.
|
||||
struct NativeGuestOutputRenderContext {
|
||||
NativeGuestOutputBackend backend = NativeGuestOutputBackend::kUnknown;
|
||||
uint32_t guest_output_width = 0;
|
||||
|
|
@ -25,59 +23,18 @@ struct NativeGuestOutputRenderContext {
|
|||
uint32_t display_width = 0;
|
||||
uint32_t display_height = 0;
|
||||
|
||||
#if REX_HAS_D3D12
|
||||
struct D3D12Context {
|
||||
d3d12::D3D12CommandProcessor* command_processor = nullptr;
|
||||
void* command_processor_user_data = nullptr;
|
||||
ID3D12Device* device = nullptr;
|
||||
ID3D12Resource* guest_output_resource = nullptr;
|
||||
DXGI_FORMAT guest_output_format = DXGI_FORMAT_UNKNOWN;
|
||||
D3D12_RESOURCE_STATES guest_output_initial_state = D3D12_RESOURCE_STATE_COMMON;
|
||||
bool (*request_one_use_view_descriptor)(void* user_data,
|
||||
D3D12_CPU_DESCRIPTOR_HANDLE* cpu_out,
|
||||
D3D12_GPU_DESCRIPTOR_HANDLE* gpu_out) = nullptr;
|
||||
bool (*create_root_signature)(void* user_data, const D3D12_ROOT_SIGNATURE_DESC* desc,
|
||||
ID3D12RootSignature** root_signature_out) = nullptr;
|
||||
bool (*push_transition_barrier)(void* user_data, ID3D12Resource* resource,
|
||||
D3D12_RESOURCE_STATES old_state,
|
||||
D3D12_RESOURCE_STATES new_state) = nullptr;
|
||||
void (*submit_barriers)(void* user_data) = nullptr;
|
||||
void (*copy_texture_region)(void* user_data, const D3D12_TEXTURE_COPY_LOCATION* dst,
|
||||
UINT dst_x, UINT dst_y, UINT dst_z,
|
||||
const D3D12_TEXTURE_COPY_LOCATION* src,
|
||||
const D3D12_BOX* src_box) = nullptr;
|
||||
void (*clear_render_target_view)(void* user_data, D3D12_CPU_DESCRIPTOR_HANDLE rtv,
|
||||
const FLOAT color[4]) = nullptr;
|
||||
void (*clear_unordered_access_view_float)(void* user_data,
|
||||
D3D12_GPU_DESCRIPTOR_HANDLE gpu_handle,
|
||||
D3D12_CPU_DESCRIPTOR_HANDLE cpu_handle,
|
||||
ID3D12Resource* resource,
|
||||
const FLOAT values[4]) = nullptr;
|
||||
void (*set_graphics_root_signature)(void* user_data,
|
||||
ID3D12RootSignature* root_signature) = nullptr;
|
||||
void (*set_graphics_root_32bit_constants)(void* user_data, UINT root_parameter_index,
|
||||
UINT num_32bit_values_to_set,
|
||||
const void* src_data,
|
||||
UINT dest_offset_in_32bit_values) = nullptr;
|
||||
void (*set_graphics_root_descriptor_table)(void* user_data, UINT root_parameter_index,
|
||||
D3D12_GPU_DESCRIPTOR_HANDLE base_descriptor) =
|
||||
nullptr;
|
||||
void (*set_pipeline_state)(void* user_data, ID3D12PipelineState* pipeline_state) = nullptr;
|
||||
void (*ia_set_primitive_topology)(void* user_data,
|
||||
D3D12_PRIMITIVE_TOPOLOGY primitive_topology) = nullptr;
|
||||
void (*ia_set_vertex_buffers)(void* user_data, UINT start_slot, UINT num_views,
|
||||
const D3D12_VERTEX_BUFFER_VIEW* views) = nullptr;
|
||||
void (*om_set_render_targets)(void* user_data, UINT num_render_target_descriptors,
|
||||
const D3D12_CPU_DESCRIPTOR_HANDLE* render_target_descriptors,
|
||||
BOOL rts_single_handle_to_descriptor_range,
|
||||
const D3D12_CPU_DESCRIPTOR_HANDLE* depth_stencil_descriptor) =
|
||||
nullptr;
|
||||
void (*rs_set_viewport)(void* user_data, const D3D12_VIEWPORT* viewport) = nullptr;
|
||||
void (*rs_set_scissor_rect)(void* user_data, const D3D12_RECT* rect) = nullptr;
|
||||
void (*draw_instanced)(void* user_data, UINT vertex_count_per_instance, UINT instance_count,
|
||||
UINT start_vertex_location, UINT start_instance_location) = nullptr;
|
||||
} d3d12;
|
||||
#endif
|
||||
// Device-level RHI: resource/pipeline creation, submission counters.
|
||||
// Stable across frames for the lifetime of the graphics system.
|
||||
nrhi::Device* device = nullptr;
|
||||
// Frame-scoped command recording into the command processor's deferred
|
||||
// command list. Only valid during the callback.
|
||||
nrhi::Cmd* cmd = nullptr;
|
||||
// The presenter's guest output image, wrapped as an RHI texture. In
|
||||
// nrhi::ResourceState::kGuestOutput at entry (or kCommon the very first
|
||||
// frame it exists); must be returned to kGuestOutput before the callback
|
||||
// returns true. The pointer is stable while the underlying image is (it
|
||||
// changes on output resize).
|
||||
nrhi::Texture* guest_output = nullptr;
|
||||
};
|
||||
|
||||
using NativeGuestOutputRenderer = bool (*)(const NativeGuestOutputRenderContext& context,
|
||||
|
|
@ -85,6 +42,9 @@ using NativeGuestOutputRenderer = bool (*)(const NativeGuestOutputRenderContext&
|
|||
|
||||
void SetNativeGuestOutputRenderer(NativeGuestOutputRenderer renderer, void* user_data);
|
||||
bool TryRenderNativeGuestOutput(const NativeGuestOutputRenderContext& context);
|
||||
// Whether a renderer is registered at all; command processors skip RHI
|
||||
// setup entirely when none is (non-Skate titles / renderer disabled).
|
||||
bool HasNativeGuestOutputRenderer();
|
||||
|
||||
// True while the registered renderer actually replaced the last presented
|
||||
// frame (false when it yields to the emulated output).
|
||||
|
|
@ -94,4 +54,18 @@ bool IsNativeGuestOutputActive();
|
|||
// fences, queries and PM4 parsing still run).
|
||||
bool ShouldSuppressEmulatedDraws();
|
||||
|
||||
// While the native guest-output renderer is active: should the emulated pass
|
||||
// currently targeting `surface_pitch`-wide surfaces be suppressed? Driven by
|
||||
// the native_render_suppress_mode cvar; shared by both command processors.
|
||||
// Draw and resolve suppression must agree: executed passes need their
|
||||
// resolves, suppressed passes leave garbage EDRAM that must never be copied
|
||||
// out.
|
||||
bool ShouldSuppressPassAtPitch(uint32_t surface_pitch);
|
||||
|
||||
// Within a suppression-EXEMPT pass: should a draw with no pixel shader
|
||||
// (depth/stencil-only: shadow casters, z-prepasses) be skipped anyway?
|
||||
// Driven by native_render_suppress_exempt_depth_only; their output feeds
|
||||
// only suppressed scene passes (the native renderer shadows itself).
|
||||
bool ShouldSuppressExemptDepthOnlyDraws();
|
||||
|
||||
} // namespace rex::graphics
|
||||
|
|
|
|||
510
include/rex/graphics/native_rhi.h
Normal file
510
include/rex/graphics/native_rhi.h
Normal file
|
|
@ -0,0 +1,510 @@
|
|||
#pragma once
|
||||
|
||||
// Native-render RHI: the backend abstraction the app-side native scene
|
||||
// renderer draws through. One implementation per SDK graphics backend
|
||||
// (d3d12/native_rhi_d3d12.cpp, vulkan/native_rhi_vulkan.cpp), each built on
|
||||
// that backend's command processor (deferred command list, barrier queue,
|
||||
// submission counters).
|
||||
//
|
||||
// The semantics deliberately mirror the D3D12 model the scene renderer was
|
||||
// written against: explicit resource states, latched command state, a
|
||||
// binding model of root constants + texture tables + direct buffer binds;
|
||||
// so the D3D12 backend is a thin passthrough and the Vulkan backend owns all
|
||||
// translation (render-pass scoping, descriptor sets, layout transitions,
|
||||
// the y-flip).
|
||||
//
|
||||
// Threading contract:
|
||||
// - Create*/Destroy*/Map/Unmap/InvalidateForRead on Device are thread-safe
|
||||
// (async decode workers create and fill resources off-thread).
|
||||
// - CreateTextureView / CreateGraphicsPipeline / CreateBindingLayout /
|
||||
// CreateShader and every Cmd method are render-thread-only.
|
||||
// - Cmd is only valid inside the native guest-output render callback.
|
||||
//
|
||||
// Alignment contract: buffer-bind offsets (SetConstantBuffer/SetBufferSrv)
|
||||
// and copy row pitches must be kRowPitchAlignment-aligned. Upload memory is
|
||||
// write-combined: never read it back; readback memory must be read through
|
||||
// InvalidateForRead.
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace rex::graphics::nrhi {
|
||||
|
||||
inline constexpr uint32_t kRowPitchAlignment = 256;
|
||||
inline constexpr uint32_t kBufferOffsetAlignment = 256;
|
||||
|
||||
enum class Backend : uint32_t {
|
||||
kD3D12,
|
||||
kVulkan,
|
||||
};
|
||||
|
||||
// Texture, view and vertex formats: exactly the set the scene renderer
|
||||
// uses. kR10G10B10A2 is the guest-output format on both backends (identical
|
||||
// bit layout: DXGI_FORMAT_R10G10B10A2_UNORM / VK_FORMAT_A2B10G10R10_UNORM_PACK32).
|
||||
enum class Format : uint32_t {
|
||||
kUnknown = 0,
|
||||
kR8G8B8A8_UNORM,
|
||||
kR8G8B8A8_UINT, // vertex blend indices
|
||||
kR8_UNORM,
|
||||
kR8G8_UNORM,
|
||||
kB5G6R5_UNORM,
|
||||
kR16G16_UNORM,
|
||||
kR10G10B10A2_UNORM,
|
||||
kR32_FLOAT,
|
||||
kR32G32_FLOAT,
|
||||
kR32G32B32_FLOAT, // vertex only
|
||||
kR32G32B32A32_FLOAT, // vertex only
|
||||
kD32_FLOAT,
|
||||
kBC1_UNORM,
|
||||
kBC2_UNORM,
|
||||
kBC3_UNORM,
|
||||
kBC4_UNORM,
|
||||
kBC5_UNORM,
|
||||
};
|
||||
|
||||
// Block dimensions/bytes for the copy math (1x1 for uncompressed).
|
||||
inline uint32_t FormatBlockWidth(Format format) {
|
||||
switch (format) {
|
||||
case Format::kBC1_UNORM:
|
||||
case Format::kBC2_UNORM:
|
||||
case Format::kBC3_UNORM:
|
||||
case Format::kBC4_UNORM:
|
||||
case Format::kBC5_UNORM:
|
||||
return 4;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// D3D12-style explicit resource states; the Vulkan backend maps each to a
|
||||
// (stage mask, access mask, image layout) triple.
|
||||
enum class ResourceState : uint32_t {
|
||||
kCommon = 0,
|
||||
kRenderTarget,
|
||||
kDepthWrite,
|
||||
kPixelShaderResource,
|
||||
kCopySource,
|
||||
kCopyDest,
|
||||
kGenericRead,
|
||||
// The presenter-owned steady state of the guest output image (D3D12:
|
||||
// the presenter's internal state constant; Vulkan: SHADER_READ_ONLY_OPTIMAL
|
||||
// for the paint pass). The guest output texture must be returned to this
|
||||
// state before the render callback returns.
|
||||
kGuestOutput,
|
||||
};
|
||||
|
||||
enum class HeapKind : uint32_t {
|
||||
kDefault = 0,
|
||||
kUpload,
|
||||
kReadback,
|
||||
};
|
||||
|
||||
enum class TextureKind : uint32_t {
|
||||
k2D = 0,
|
||||
kCube,
|
||||
k3D,
|
||||
};
|
||||
|
||||
// Usage flags beyond "sampled + upload destination" (always granted).
|
||||
enum TextureUsage : uint32_t {
|
||||
kTextureUsageNone = 0,
|
||||
kTextureUsageRenderTarget = 1u << 0,
|
||||
kTextureUsageDepthStencil = 1u << 1,
|
||||
kTextureUsageCopySource = 1u << 2, // readback source
|
||||
};
|
||||
|
||||
enum class ViewDimension : uint32_t {
|
||||
k2D = 0,
|
||||
k2DMS,
|
||||
kCube,
|
||||
k3D,
|
||||
};
|
||||
|
||||
// Per-channel view swizzle (Xenos fetch-constant swizzles).
|
||||
enum class Swizzle : uint8_t {
|
||||
kX = 0,
|
||||
kY,
|
||||
kZ,
|
||||
kW,
|
||||
kZero,
|
||||
kOne,
|
||||
};
|
||||
|
||||
enum class CompareFunc : uint32_t {
|
||||
kAlways = 0,
|
||||
kLess,
|
||||
kLessEqual,
|
||||
};
|
||||
|
||||
enum class CullMode : uint32_t {
|
||||
kNone = 0,
|
||||
kFront,
|
||||
kBack,
|
||||
};
|
||||
|
||||
enum class BlendFactor : uint32_t {
|
||||
kZero = 0,
|
||||
kOne,
|
||||
kSrcAlpha,
|
||||
kInvSrcAlpha,
|
||||
};
|
||||
|
||||
enum class BlendOp : uint32_t {
|
||||
kAdd = 0,
|
||||
kMin,
|
||||
};
|
||||
|
||||
enum class PrimitiveTopology : uint32_t {
|
||||
kTriangleList = 0,
|
||||
kTriangleStrip,
|
||||
};
|
||||
|
||||
enum class Filter : uint32_t {
|
||||
kPoint = 0,
|
||||
kLinear,
|
||||
kAnisotropic,
|
||||
};
|
||||
|
||||
enum class AddressMode : uint32_t {
|
||||
kWrap = 0,
|
||||
kClamp,
|
||||
};
|
||||
|
||||
enum class ShaderStage : uint32_t {
|
||||
kVertex = 0,
|
||||
kPixel,
|
||||
};
|
||||
|
||||
// Shader visibility of a binding parameter (D3D12 root-parameter visibility /
|
||||
// Vulkan stage flags).
|
||||
enum class Visibility : uint32_t {
|
||||
kAll = 0,
|
||||
kVertex,
|
||||
kPixel,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Opaque handles. Created and destroyed only through Device; destruction is
|
||||
// always deferred against the submission timeline.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class Buffer {
|
||||
public:
|
||||
virtual uint64_t size() const = 0;
|
||||
|
||||
protected:
|
||||
virtual ~Buffer() = default;
|
||||
};
|
||||
|
||||
class Texture {
|
||||
public:
|
||||
virtual uint32_t width() const = 0;
|
||||
virtual uint32_t height() const = 0;
|
||||
virtual Format format() const = 0;
|
||||
|
||||
protected:
|
||||
virtual ~Texture() = default;
|
||||
};
|
||||
|
||||
class TextureView {
|
||||
protected:
|
||||
virtual ~TextureView() = default;
|
||||
};
|
||||
|
||||
class BindingLayout {
|
||||
protected:
|
||||
virtual ~BindingLayout() = default;
|
||||
};
|
||||
|
||||
class Shader {
|
||||
protected:
|
||||
virtual ~Shader() = default;
|
||||
};
|
||||
|
||||
class Pipeline {
|
||||
protected:
|
||||
virtual ~Pipeline() = default;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Descriptors.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct BufferDesc {
|
||||
uint64_t size = 0;
|
||||
HeapKind heap = HeapKind::kDefault;
|
||||
};
|
||||
|
||||
struct TextureDesc {
|
||||
TextureKind kind = TextureKind::k2D;
|
||||
uint32_t width = 1;
|
||||
uint32_t height = 1;
|
||||
uint32_t depth = 1; // k3D only
|
||||
uint32_t mip_levels = 1;
|
||||
uint32_t sample_count = 1;
|
||||
Format format = Format::kUnknown;
|
||||
uint32_t usage = kTextureUsageNone; // TextureUsage bits
|
||||
ResourceState initial_state = ResourceState::kCopyDest;
|
||||
// Optimized clear value for render/depth targets (D3D12 fast clears).
|
||||
float clear_color[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
float clear_depth = 1.0f;
|
||||
};
|
||||
|
||||
struct TextureViewDesc {
|
||||
ViewDimension dimension = ViewDimension::k2D;
|
||||
// kUnknown = the texture's own format (D32_FLOAT textures are viewed as
|
||||
// R32_FLOAT automatically).
|
||||
Format format = Format::kUnknown;
|
||||
uint32_t base_mip = 0;
|
||||
uint32_t mip_levels = ~0u; // ~0u = all remaining
|
||||
Swizzle swizzle[4] = {Swizzle::kX, Swizzle::kY, Swizzle::kZ, Swizzle::kW};
|
||||
};
|
||||
|
||||
enum class BindingParamKind : uint32_t {
|
||||
// 32-bit root constants (register b<slot>). One per layout at most. On
|
||||
// Vulkan this is backed by an internal per-draw ring UBO.
|
||||
kConstants = 0,
|
||||
// Constant buffer bound by (buffer, offset).
|
||||
kConstantBuffer,
|
||||
// Read-only structured/raw buffer bound by (buffer, offset).
|
||||
kBufferSrv,
|
||||
// Table of 1..8 sampled textures at consecutive t-registers, bound with
|
||||
// SetTexture / SetTexturePair / SetTextures.
|
||||
kTextureTable,
|
||||
};
|
||||
|
||||
inline constexpr uint32_t kMaxTextureTableSize = 8;
|
||||
|
||||
struct BindingParamDesc {
|
||||
BindingParamKind kind = BindingParamKind::kTextureTable;
|
||||
uint32_t shader_register = 0; // b/t register of the first entry
|
||||
uint32_t count = 1; // kConstants: number of 32-bit values;
|
||||
// kTextureTable: 1 or 2 textures
|
||||
Visibility visibility = Visibility::kAll;
|
||||
};
|
||||
|
||||
struct StaticSamplerDesc {
|
||||
uint32_t shader_register = 0; // s register
|
||||
Filter filter = Filter::kLinear;
|
||||
AddressMode address = AddressMode::kClamp;
|
||||
uint32_t max_anisotropy = 1;
|
||||
};
|
||||
|
||||
inline constexpr uint32_t kMaxBindingParams = 12;
|
||||
inline constexpr uint32_t kMaxStaticSamplers = 4;
|
||||
|
||||
struct BindingLayoutDesc {
|
||||
BindingParamDesc params[kMaxBindingParams] = {};
|
||||
uint32_t param_count = 0;
|
||||
StaticSamplerDesc static_samplers[kMaxStaticSamplers] = {};
|
||||
uint32_t static_sampler_count = 0;
|
||||
// D3D12: whether the root signature allows an input-assembler layout.
|
||||
bool allow_input_layout = true;
|
||||
};
|
||||
|
||||
struct ShaderMacro {
|
||||
const char* name = nullptr;
|
||||
const char* value = nullptr;
|
||||
};
|
||||
|
||||
struct ShaderDesc {
|
||||
ShaderStage stage = ShaderStage::kVertex;
|
||||
// Debug identity, used in compile-error logs.
|
||||
const char* name = nullptr;
|
||||
// HLSL source + entry point; the D3D12 backend compiles this at runtime
|
||||
// (vs_5_0 / ps_5_0), exactly as the scene renderer always has.
|
||||
const char* hlsl_source = nullptr;
|
||||
const char* entry_point = nullptr;
|
||||
const ShaderMacro* macros = nullptr; // terminated by a {nullptr, nullptr}
|
||||
// Prebuilt SPIR-V for the same (source, entry, macros), generated
|
||||
// offline by the app's shader script; required by the Vulkan backend.
|
||||
const uint32_t* spirv = nullptr;
|
||||
size_t spirv_size_bytes = 0;
|
||||
};
|
||||
|
||||
struct InputElementDesc {
|
||||
// D3D12 semantic binding.
|
||||
const char* semantic_name = nullptr;
|
||||
uint32_t semantic_index = 0;
|
||||
// Vulkan input location: the element's declaration order in the VS input
|
||||
// struct (dxc assigns locations in that order).
|
||||
uint32_t location = 0;
|
||||
Format format = Format::kUnknown;
|
||||
uint32_t byte_offset = 0;
|
||||
};
|
||||
|
||||
struct BlendStateDesc {
|
||||
bool enable = false;
|
||||
BlendFactor src = BlendFactor::kOne;
|
||||
BlendFactor dst = BlendFactor::kZero;
|
||||
BlendOp op = BlendOp::kAdd;
|
||||
BlendFactor src_alpha = BlendFactor::kOne;
|
||||
BlendFactor dst_alpha = BlendFactor::kZero;
|
||||
BlendOp op_alpha = BlendOp::kAdd;
|
||||
uint8_t write_mask = 0xF; // RGBA bits 0..3
|
||||
};
|
||||
|
||||
struct DepthStateDesc {
|
||||
bool test_enable = false;
|
||||
bool write_enable = false;
|
||||
CompareFunc func = CompareFunc::kLess;
|
||||
};
|
||||
|
||||
struct GraphicsPipelineDesc {
|
||||
BindingLayout* layout = nullptr;
|
||||
Shader* vs = nullptr;
|
||||
Shader* ps = nullptr;
|
||||
const InputElementDesc* input_elements = nullptr; // nullptr = SV_VertexID
|
||||
uint32_t input_element_count = 0;
|
||||
uint32_t vertex_stride = 0;
|
||||
BlendStateDesc blend;
|
||||
DepthStateDesc depth;
|
||||
CullMode cull = CullMode::kNone;
|
||||
bool depth_clip = true;
|
||||
Format rtv_format = Format::kUnknown; // kUnknown = no color target
|
||||
Format dsv_format = Format::kUnknown; // kUnknown = no depth target
|
||||
uint32_t sample_count = 1;
|
||||
};
|
||||
|
||||
struct Viewport {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
float width = 0.0f;
|
||||
float height = 0.0f;
|
||||
float min_depth = 0.0f;
|
||||
float max_depth = 1.0f;
|
||||
};
|
||||
|
||||
struct Rect {
|
||||
int32_t left = 0;
|
||||
int32_t top = 0;
|
||||
int32_t right = 0;
|
||||
int32_t bottom = 0;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Command recording. Wraps the command processor's deferred command list for
|
||||
// the current frame; valid only inside the render callback, render thread
|
||||
// only. State latches like a D3D12 command list; SetBindingLayout resets
|
||||
// root-constant and binding state.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class Cmd {
|
||||
public:
|
||||
virtual void SetBindingLayout(BindingLayout* layout) = 0;
|
||||
virtual void SetPipeline(Pipeline* pipeline) = 0;
|
||||
|
||||
// kConstants param. Values persist across pipeline changes within the
|
||||
// current layout (D3D12 root-constant semantics).
|
||||
virtual void SetRootConstants(uint32_t param, uint32_t count,
|
||||
const void* values,
|
||||
uint32_t dest_offset_in_values) = 0;
|
||||
// offset must be kBufferOffsetAlignment-aligned.
|
||||
virtual void SetConstantBuffer(uint32_t param, Buffer* buffer,
|
||||
uint64_t offset) = 0;
|
||||
virtual void SetBufferSrv(uint32_t param, Buffer* buffer,
|
||||
uint64_t offset) = 0;
|
||||
virtual void SetTexture(uint32_t param, TextureView* view) = 0;
|
||||
// Multi-entry kTextureTable params. Binding fewer views than the table
|
||||
// declares leaves the tail entries at a backend fallback (white);
|
||||
// shaders only sample declared-but-unbound slots behind flags (the old
|
||||
// D3D12 "dangling t5" arrangement, made explicit).
|
||||
virtual void SetTexturePair(uint32_t param, TextureView* first,
|
||||
TextureView* second) = 0;
|
||||
virtual void SetTextures(uint32_t param, TextureView* const* views,
|
||||
uint32_t count) = 0;
|
||||
|
||||
// Mip 0 of each. color/depth may each be nullptr.
|
||||
virtual void SetRenderTargets(Texture* color, Texture* depth) = 0;
|
||||
virtual void ClearRenderTarget(Texture* color, const float color4[4]) = 0;
|
||||
virtual void ClearDepth(Texture* depth, float value) = 0;
|
||||
|
||||
virtual void SetViewport(const Viewport& viewport) = 0;
|
||||
virtual void SetScissor(const Rect& rect) = 0;
|
||||
|
||||
virtual void SetVertexBuffer(Buffer* buffer, uint64_t offset,
|
||||
uint32_t size_bytes, uint32_t stride) = 0;
|
||||
// 16-bit indices (the only index format the scene uses).
|
||||
virtual void SetIndexBuffer(Buffer* buffer, uint64_t offset,
|
||||
uint32_t size_bytes) = 0;
|
||||
virtual void SetPrimitiveTopology(PrimitiveTopology topology) = 0;
|
||||
|
||||
virtual void Draw(uint32_t vertex_count, uint32_t start_vertex) = 0;
|
||||
virtual void DrawIndexed(uint32_t index_count, uint32_t start_index,
|
||||
int32_t base_vertex) = 0;
|
||||
|
||||
// Copies. row_pitch must be kRowPitchAlignment-aligned. width/height are
|
||||
// the explicit footprint dims in texels (block-aligned for BC formats,
|
||||
// exactly as the D3D12 placed footprints were built); depth is 1 except
|
||||
// for k3D uploads. array_slice selects the cube face for kCube textures.
|
||||
virtual void CopyBufferToTexture(Texture* dst, uint32_t mip,
|
||||
uint32_t array_slice, Buffer* src,
|
||||
uint64_t src_offset, uint32_t row_pitch,
|
||||
uint32_t width, uint32_t height,
|
||||
uint32_t depth) = 0;
|
||||
virtual void CopyTextureToBuffer(Buffer* dst, uint64_t dst_offset,
|
||||
uint32_t row_pitch, Texture* src,
|
||||
uint32_t mip, uint32_t width,
|
||||
uint32_t height) = 0;
|
||||
|
||||
// Queues a transition barrier; FlushBarriers() submits queued barriers
|
||||
// (and, on Vulkan, ends any open render pass).
|
||||
virtual void Barrier(Texture* texture, ResourceState before,
|
||||
ResourceState after) = 0;
|
||||
virtual void FlushBarriers() = 0;
|
||||
|
||||
protected:
|
||||
virtual ~Cmd() = default;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Device: creation, destruction (always deferred), submission timeline.
|
||||
// Owned by the command processor; outlives every frame's Cmd.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class Device {
|
||||
public:
|
||||
virtual Backend backend() const = 0;
|
||||
|
||||
// --- thread-safe ---
|
||||
virtual Buffer* CreateBuffer(const BufferDesc& desc) = 0;
|
||||
virtual Texture* CreateTexture(const TextureDesc& desc) = 0;
|
||||
// Persistent mapping is allowed (and the norm). Upload memory is
|
||||
// write-combined: write-only, no reads.
|
||||
virtual void* Map(Buffer* buffer) = 0;
|
||||
virtual void Unmap(Buffer* buffer) = 0;
|
||||
// Make GPU writes visible before reading mapped readback memory
|
||||
// (no-op on D3D12, cache invalidate on non-coherent Vulkan memory).
|
||||
virtual void InvalidateForRead(Buffer* buffer, uint64_t offset,
|
||||
uint64_t size) = 0;
|
||||
|
||||
// Deferred destruction: the object dies when the current submission
|
||||
// completes. Views/bindings referencing a destroyed object are retired by
|
||||
// the backend. Null is allowed (no-op).
|
||||
virtual void DestroyDeferred(Buffer* buffer) = 0;
|
||||
virtual void DestroyDeferred(Texture* texture) = 0;
|
||||
virtual void DestroyDeferred(TextureView* view) = 0;
|
||||
virtual void DestroyDeferred(Pipeline* pipeline) = 0;
|
||||
virtual void DestroyDeferred(Shader* shader) = 0;
|
||||
|
||||
// --- render-thread-only ---
|
||||
virtual TextureView* CreateTextureView(Texture* texture,
|
||||
const TextureViewDesc& desc) = 0;
|
||||
virtual BindingLayout* CreateBindingLayout(const BindingLayoutDesc& desc) = 0;
|
||||
// Returns nullptr on compile failure (details logged).
|
||||
virtual Shader* CreateShader(const ShaderDesc& desc) = 0;
|
||||
virtual Pipeline* CreateGraphicsPipeline(const GraphicsPipelineDesc& desc) = 0;
|
||||
|
||||
// Largest supported sample count <= desired for a render-target format.
|
||||
virtual uint32_t GetSupportedSampleCount(Format format,
|
||||
uint32_t desired) = 0;
|
||||
|
||||
// The command processor's monotonic submission counters: the basis of all
|
||||
// lifetime/readback gating.
|
||||
virtual uint64_t CurrentSubmission() const = 0;
|
||||
virtual uint64_t CompletedSubmission() const = 0;
|
||||
|
||||
protected:
|
||||
virtual ~Device() = default;
|
||||
};
|
||||
|
||||
} // namespace rex::graphics::nrhi
|
||||
|
|
@ -44,6 +44,10 @@
|
|||
#include <rex/ui/vulkan/provider.h>
|
||||
#include <rex/ui/vulkan/upload_buffer_pool.h>
|
||||
|
||||
namespace rex::graphics::nrhi {
|
||||
class Device;
|
||||
}
|
||||
|
||||
namespace rex::graphics::vulkan {
|
||||
|
||||
class VulkanCommandProcessor : public CommandProcessor {
|
||||
|
|
@ -611,6 +615,10 @@ class VulkanCommandProcessor : public CommandProcessor {
|
|||
|
||||
std::unique_ptr<ui::vulkan::VulkanUploadBufferPool> uniform_buffer_pool_;
|
||||
|
||||
// Native-render RHI device (rex/graphics/native_rhi.h), created lazily on
|
||||
// the first swap with a native guest-output renderer registered.
|
||||
nrhi::Device* native_rhi_device_ = nullptr;
|
||||
|
||||
// Descriptor set layouts used by different shaders.
|
||||
VkDescriptorSetLayout descriptor_set_layout_empty_ = VK_NULL_HANDLE;
|
||||
VkDescriptorSetLayout descriptor_set_layout_constants_ = VK_NULL_HANDLE;
|
||||
|
|
@ -842,6 +850,9 @@ class VulkanCommandProcessor : public CommandProcessor {
|
|||
// timestamps are enabled.
|
||||
static constexpr uint32_t kGpuProfileLogFrameInterval = 120;
|
||||
uint64_t gpu_profile_bucket_us_[size_t(rex::perf::DrawBucket::kCount)] = {};
|
||||
// Interval counts per bucket over the same logging window (distinguishes
|
||||
// "more intervals" from "same intervals, slower").
|
||||
uint64_t gpu_profile_bucket_n_[size_t(rex::perf::DrawBucket::kCount)] = {};
|
||||
uint64_t gpu_profile_span_us_ = 0;
|
||||
uint64_t gpu_profile_submissions_ = 0;
|
||||
uint32_t gpu_profile_frames_ = 0;
|
||||
|
|
|
|||
|
|
@ -232,6 +232,26 @@ class DeferredCommandBuffer {
|
|||
regions, sizeof(VkBufferImageCopy) * region_count);
|
||||
}
|
||||
|
||||
VkBufferImageCopy* CmdCopyImageToBufferEmplace(VkImage src_image, VkImageLayout src_image_layout,
|
||||
VkBuffer dst_buffer, uint32_t region_count) {
|
||||
const size_t header_size =
|
||||
rex::align(sizeof(ArgsVkCopyImageToBuffer), alignof(VkBufferImageCopy));
|
||||
uint8_t* args_ptr = reinterpret_cast<uint8_t*>(WriteCommand(
|
||||
Command::kVkCopyImageToBuffer, header_size + sizeof(VkBufferImageCopy) * region_count));
|
||||
auto& args = *reinterpret_cast<ArgsVkCopyImageToBuffer*>(args_ptr);
|
||||
args.src_image = src_image;
|
||||
args.src_image_layout = src_image_layout;
|
||||
args.dst_buffer = dst_buffer;
|
||||
args.region_count = region_count;
|
||||
return reinterpret_cast<VkBufferImageCopy*>(args_ptr + header_size);
|
||||
}
|
||||
void CmdVkCopyImageToBuffer(VkImage src_image, VkImageLayout src_image_layout,
|
||||
VkBuffer dst_buffer, uint32_t region_count,
|
||||
const VkBufferImageCopy* regions) {
|
||||
std::memcpy(CmdCopyImageToBufferEmplace(src_image, src_image_layout, dst_buffer, region_count),
|
||||
regions, sizeof(VkBufferImageCopy) * region_count);
|
||||
}
|
||||
|
||||
void CmdVkCopyQueryPoolResults(VkQueryPool query_pool, uint32_t first_query, uint32_t query_count,
|
||||
VkBuffer dst_buffer, VkDeviceSize dst_offset, VkDeviceSize stride,
|
||||
VkQueryResultFlags flags) {
|
||||
|
|
@ -418,6 +438,7 @@ class DeferredCommandBuffer {
|
|||
kVkClearDepthStencilImage,
|
||||
kVkCopyBuffer,
|
||||
kVkCopyBufferToImage,
|
||||
kVkCopyImageToBuffer,
|
||||
kVkCopyQueryPoolResults,
|
||||
kVkDispatch,
|
||||
kVkDraw,
|
||||
|
|
@ -550,6 +571,15 @@ class DeferredCommandBuffer {
|
|||
static_assert(alignof(VkBufferImageCopy) <= alignof(uintmax_t));
|
||||
};
|
||||
|
||||
struct ArgsVkCopyImageToBuffer {
|
||||
VkImage src_image;
|
||||
VkImageLayout src_image_layout;
|
||||
VkBuffer dst_buffer;
|
||||
uint32_t region_count;
|
||||
// Followed by aligned VkBufferImageCopy[].
|
||||
static_assert(alignof(VkBufferImageCopy) <= alignof(uintmax_t));
|
||||
};
|
||||
|
||||
struct ArgsVkCopyQueryPoolResults {
|
||||
VkQueryPool query_pool;
|
||||
uint32_t first_query;
|
||||
|
|
|
|||
45
include/rex/graphics/vulkan/native_rhi_vulkan.h
Normal file
45
include/rex/graphics/vulkan/native_rhi_vulkan.h
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
#pragma once
|
||||
|
||||
// Vulkan implementation of the native-render RHI (rex/graphics/native_rhi.h).
|
||||
// Constructed and owned by the Vulkan command processor; built on the CP's
|
||||
// deferred command buffer, its barrier queue and its submission counters.
|
||||
// Unlike the D3D12 backend (a thin passthrough), this backend owns all the
|
||||
// translation the interface promises: render-pass scoping, descriptor sets
|
||||
// per the frozen set/binding plan, explicit-state -> (stage, access,
|
||||
// layout) mapping, and the
|
||||
// negative-viewport y-flip.
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include <rex/graphics/native_rhi.h>
|
||||
#include <rex/ui/vulkan/api.h>
|
||||
|
||||
namespace rex::graphics::vulkan {
|
||||
|
||||
class VulkanCommandProcessor;
|
||||
|
||||
// Creates the device wrapper (call once; destroy with DestroyNativeRhiDevice
|
||||
// after all GPU work completed - AwaitAllQueueOperationsCompletion).
|
||||
nrhi::Device* CreateNativeRhiDevice(VulkanCommandProcessor* command_processor);
|
||||
void DestroyNativeRhiDevice(nrhi::Device* device);
|
||||
|
||||
// Per-frame, inside the guest-output refresher (submission open): wraps the
|
||||
// presenter's guest output image (cached by VkImage identity), drains
|
||||
// completed retirement, advances the internal root-constant ring region and
|
||||
// resets the frame Cmd latches. guest_output_ever_written_previously selects
|
||||
// the wrapper's initial tracked layout (UNDEFINED on the image's first ever
|
||||
// write, the presenter's internal SHADER_READ_ONLY_OPTIMAL otherwise).
|
||||
// guest_output_out receives the wrapped texture.
|
||||
nrhi::Cmd* NativeRhiBeginFrame(nrhi::Device* device, VkImage guest_output_image,
|
||||
VkImageView guest_output_image_view,
|
||||
bool guest_output_ever_written_previously, uint32_t width,
|
||||
uint32_t height, nrhi::Texture** guest_output_out);
|
||||
|
||||
// Vulkan-only frame epilogue: ends any render pass still open in the frame
|
||||
// Cmd, flushes pending clears that no draw consumed, and submits the command
|
||||
// processor's queued barriers (including the app's release barrier of the
|
||||
// guest output back to nrhi::ResourceState::kGuestOutput). Call after the
|
||||
// native render callback, before EndSubmission.
|
||||
void NativeRhiEndFrame(nrhi::Device* device);
|
||||
|
||||
} // namespace rex::graphics::vulkan
|
||||
|
|
@ -60,6 +60,9 @@ enum class DrawBucket : uint8_t {
|
|||
// The buffer-to-image copy phase of a texture upload (kTextureUpload then
|
||||
// covers only the load compute dispatches and their barriers).
|
||||
kTextureUploadCopy,
|
||||
// The native guest-output renderer's whole GPU pass (scene + 2D replay +
|
||||
// resolve), recorded inside IssueSwap. Runs until frame end.
|
||||
kNativeScene,
|
||||
kCount,
|
||||
};
|
||||
|
||||
|
|
@ -98,6 +101,8 @@ inline const char* DrawBucketName(DrawBucket bucket) {
|
|||
return "ResolveGap";
|
||||
case DrawBucket::kTextureUploadCopy:
|
||||
return "TexUpCopy";
|
||||
case DrawBucket::kNativeScene:
|
||||
return "Native";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ XE_UI_VULKAN_FUNCTION(vkDestroySemaphore)
|
|||
XE_UI_VULKAN_FUNCTION(vkDestroyShaderModule)
|
||||
XE_UI_VULKAN_FUNCTION(vkEndCommandBuffer)
|
||||
XE_UI_VULKAN_FUNCTION(vkFlushMappedMemoryRanges)
|
||||
XE_UI_VULKAN_FUNCTION(vkFreeDescriptorSets)
|
||||
XE_UI_VULKAN_FUNCTION(vkFreeMemory)
|
||||
XE_UI_VULKAN_FUNCTION(vkGetBufferMemoryRequirements)
|
||||
XE_UI_VULKAN_FUNCTION(vkGetDeviceQueue)
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ set(REXGRAPHICS_VULKAN_SOURCES
|
|||
vulkan/graphics_system.cpp
|
||||
vulkan/deferred_command_buffer.cpp
|
||||
vulkan/command_processor.cpp
|
||||
vulkan/native_rhi_vulkan.cpp
|
||||
vulkan/pipeline_cache.cpp
|
||||
vulkan/primitive_processor.cpp
|
||||
vulkan/render_target_cache.cpp
|
||||
|
|
@ -80,6 +81,7 @@ if(REXGLUE_USE_D3D12)
|
|||
set(REXGRAPHICS_D3D12_SOURCES
|
||||
d3d12/graphics_system.cpp
|
||||
d3d12/command_processor.cpp
|
||||
d3d12/native_rhi_d3d12.cpp
|
||||
d3d12/primitive_processor.cpp
|
||||
d3d12/render_target_cache.cpp
|
||||
d3d12/shader.cpp
|
||||
|
|
@ -146,6 +148,7 @@ if(REXGLUE_USE_D3D12)
|
|||
d3d12
|
||||
dxgi
|
||||
dxguid
|
||||
d3dcompiler # native-render RHI runtime HLSL compilation
|
||||
)
|
||||
target_compile_definitions(rexgraphics PUBLIC REX_HAS_D3D12=1)
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
#include <rex/dbg.h>
|
||||
#include <rex/perf/counter.h>
|
||||
#include <rex/graphics/d3d12/command_processor.h>
|
||||
#include <rex/graphics/d3d12/native_rhi_d3d12.h>
|
||||
#include <rex/graphics/native_guest_renderer.h>
|
||||
#include <rex/graphics/d3d12/graphics_system.h>
|
||||
#include <rex/graphics/d3d12/shader.h>
|
||||
|
|
@ -40,61 +41,10 @@
|
|||
REXCVAR_DEFINE_BOOL(d3d12_bindless, true, "GPU/D3D12", "Use bindless resources where available")
|
||||
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
|
||||
|
||||
REXCVAR_DEFINE_INT32(native_render_suppress_mode, 2, "GPU",
|
||||
"Which emulated passes to suppress while the native guest-output "
|
||||
"renderer is active. 0 = framebuffer-sized passes only (surface "
|
||||
"pitch >= 1280); the game's whole postfx chain then still executes "
|
||||
"at 1152x640 x resolution scale EVERY frame and paces the pipeline "
|
||||
"(~half the achievable frame rate). 1 = suppress everything (perf "
|
||||
"probing; mid-gameplay lightmap page composition breaks). 2 = "
|
||||
"suppress everything EXCEPT the memory-composition passes whose "
|
||||
"outputs the native renderer samples from guest memory: lightmap "
|
||||
"page composition (pitch 1024) and small composite surfaces (pitch "
|
||||
"<= 512, CAS outfit pieces). Menus/pause/loading always render "
|
||||
"fully (the native renderer yields there), so shop/outfit "
|
||||
"composition is unaffected by any mode. 3 = portrait-window mode: "
|
||||
"like 0 the sub-framebuffer RTT passes execute (one-shot frontend "
|
||||
"portrait renders, census pitches 560-1200), but the 1152-wide "
|
||||
"main scene + postfx band stays suppressed; that band is the "
|
||||
"whole-pipeline cost at scaled resolutions and portraits never "
|
||||
"need it.")
|
||||
.range(0, 3)
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
|
||||
REXCVAR_DEFINE_BOOL(d3d12_readback_memexport, false, "GPU/D3D12",
|
||||
"Read data written by memory export in shaders on the CPU")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
|
||||
namespace {
|
||||
// While the native guest-output renderer is active: should the emulated pass
|
||||
// currently targeting `surface_pitch`-wide surfaces be suppressed? See
|
||||
// native_render_suppress_mode. Draw and resolve suppression must agree:
|
||||
// executed passes need their resolves, suppressed passes leave garbage EDRAM
|
||||
// that must never be copied out.
|
||||
bool ShouldSuppressPassAtPitch(uint32_t surface_pitch) {
|
||||
switch (REXCVAR_GET(native_render_suppress_mode)) {
|
||||
case 0:
|
||||
return surface_pitch >= 1280;
|
||||
case 1:
|
||||
return true;
|
||||
case 3:
|
||||
// Portrait-window mode: the one-shot frontend portrait RTTs (Skate 3
|
||||
// census during the window: 1200/800/640/600/560 + small mips)
|
||||
// execute, while the 1152-wide main scene/postfx band, the
|
||||
// whole-pipeline cost at scaled resolutions, stays suppressed like
|
||||
// the framebuffer.
|
||||
return surface_pitch >= 1280 || surface_pitch == 1152;
|
||||
default:
|
||||
// Execute only the memory-composition passes the native renderer
|
||||
// samples: lightmap page composition (1024) and small composite
|
||||
// surfaces (<= 512, CAS outfit pieces). The <= 512 window also lets a
|
||||
// few tiny postfx pyramid mips through, negligible, and safer than
|
||||
// guessing which small surfaces matter.
|
||||
return !(surface_pitch == 1024 || surface_pitch <= 512);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
REXCVAR_DEFINE_BOOL(d3d12_readback_resolve, false, "GPU/D3D12",
|
||||
"Read render-to-texture results on the CPU")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
|
|
@ -2036,6 +1986,11 @@ void D3D12CommandProcessor::ShutdownContext() {
|
|||
ShutdownGpuTimestampResources();
|
||||
ShutdownOcclusionQueryResources();
|
||||
|
||||
if (native_rhi_device_ != nullptr) {
|
||||
DestroyNativeRhiDevice(native_rhi_device_);
|
||||
native_rhi_device_ = nullptr;
|
||||
}
|
||||
|
||||
ui::d3d12::util::ReleaseAndNull(readback_buffer_);
|
||||
readback_buffer_size_ = 0;
|
||||
for (auto& resolve_readback_pair : readback_buffers_) {
|
||||
|
|
@ -2457,142 +2412,26 @@ void D3D12CommandProcessor::IssueSwap(uint32_t frontbuffer_ptr, uint32_t frontbu
|
|||
static_cast<ui::d3d12::D3D12Presenter::D3D12GuestOutputRefreshContext&>(context)
|
||||
.resource_uav_capable();
|
||||
|
||||
NativeGuestOutputRenderContext native_context;
|
||||
native_context.backend = NativeGuestOutputBackend::kD3D12;
|
||||
native_context.guest_output_width = guest_output_width;
|
||||
native_context.guest_output_height = guest_output_height;
|
||||
native_context.display_width = display_width;
|
||||
native_context.display_height = display_height;
|
||||
native_context.d3d12.command_processor = this;
|
||||
native_context.d3d12.command_processor_user_data = this;
|
||||
native_context.d3d12.device = device;
|
||||
native_context.d3d12.guest_output_resource = guest_output_resource;
|
||||
native_context.d3d12.guest_output_format = ui::d3d12::D3D12Presenter::kGuestOutputFormat;
|
||||
native_context.d3d12.guest_output_initial_state =
|
||||
ui::d3d12::D3D12Presenter::kGuestOutputInternalState;
|
||||
native_context.d3d12.request_one_use_view_descriptor =
|
||||
[](void* user_data, D3D12_CPU_DESCRIPTOR_HANDLE* cpu_out,
|
||||
D3D12_GPU_DESCRIPTOR_HANDLE* gpu_out) -> bool {
|
||||
auto* command_processor = static_cast<D3D12CommandProcessor*>(user_data);
|
||||
ui::d3d12::util::DescriptorCpuGpuHandlePair descriptor;
|
||||
if (!command_processor->RequestOneUseSingleViewDescriptors(1, &descriptor)) {
|
||||
return false;
|
||||
if (HasNativeGuestOutputRenderer()) {
|
||||
NativeGuestOutputRenderContext native_context;
|
||||
native_context.backend = NativeGuestOutputBackend::kD3D12;
|
||||
native_context.guest_output_width = guest_output_width;
|
||||
native_context.guest_output_height = guest_output_height;
|
||||
native_context.display_width = display_width;
|
||||
native_context.display_height = display_height;
|
||||
if (native_rhi_device_ == nullptr) {
|
||||
native_rhi_device_ = CreateNativeRhiDevice(this);
|
||||
}
|
||||
native_context.device = native_rhi_device_;
|
||||
native_context.cmd = NativeRhiBeginFrame(
|
||||
native_rhi_device_, guest_output_resource,
|
||||
ui::d3d12::D3D12Presenter::kGuestOutputFormat,
|
||||
ui::d3d12::D3D12Presenter::kGuestOutputInternalState, guest_output_width,
|
||||
guest_output_height, &native_context.guest_output);
|
||||
if (TryRenderNativeGuestOutput(native_context)) {
|
||||
EndSubmission(true);
|
||||
return true;
|
||||
}
|
||||
*cpu_out = descriptor.first;
|
||||
*gpu_out = descriptor.second;
|
||||
return true;
|
||||
};
|
||||
native_context.d3d12.create_root_signature =
|
||||
[](void* user_data, const D3D12_ROOT_SIGNATURE_DESC* desc,
|
||||
ID3D12RootSignature** root_signature_out) -> bool {
|
||||
auto* command_processor = static_cast<D3D12CommandProcessor*>(user_data);
|
||||
*root_signature_out =
|
||||
ui::d3d12::util::CreateRootSignature(command_processor->GetD3D12Provider(), *desc);
|
||||
return *root_signature_out != nullptr;
|
||||
};
|
||||
native_context.d3d12.push_transition_barrier =
|
||||
[](void* user_data, ID3D12Resource* resource, D3D12_RESOURCE_STATES old_state,
|
||||
D3D12_RESOURCE_STATES new_state) -> bool {
|
||||
return static_cast<D3D12CommandProcessor*>(user_data)->PushTransitionBarrier(
|
||||
resource, old_state, new_state);
|
||||
};
|
||||
native_context.d3d12.submit_barriers = [](void* user_data) {
|
||||
static_cast<D3D12CommandProcessor*>(user_data)->SubmitBarriers();
|
||||
};
|
||||
native_context.d3d12.copy_texture_region =
|
||||
[](void* user_data, const D3D12_TEXTURE_COPY_LOCATION* dst, UINT dst_x, UINT dst_y,
|
||||
UINT dst_z, const D3D12_TEXTURE_COPY_LOCATION* src, const D3D12_BOX* src_box) {
|
||||
static_cast<D3D12CommandProcessor*>(user_data)
|
||||
->GetDeferredCommandList()
|
||||
.D3DCopyTextureRegion(dst, dst_x, dst_y, dst_z, src, src_box);
|
||||
};
|
||||
native_context.d3d12.clear_render_target_view =
|
||||
[](void* user_data, D3D12_CPU_DESCRIPTOR_HANDLE rtv, const FLOAT color[4]) {
|
||||
static_cast<D3D12CommandProcessor*>(user_data)
|
||||
->GetDeferredCommandList()
|
||||
.D3DClearRenderTargetView(rtv, color, 0, nullptr);
|
||||
};
|
||||
native_context.d3d12.clear_unordered_access_view_float =
|
||||
[](void* user_data, D3D12_GPU_DESCRIPTOR_HANDLE gpu_handle,
|
||||
D3D12_CPU_DESCRIPTOR_HANDLE cpu_handle, ID3D12Resource* resource,
|
||||
const FLOAT values[4]) {
|
||||
static_cast<D3D12CommandProcessor*>(user_data)
|
||||
->GetDeferredCommandList()
|
||||
.D3DClearUnorderedAccessViewFloat(gpu_handle, cpu_handle, resource, values, 0,
|
||||
nullptr);
|
||||
};
|
||||
native_context.d3d12.set_graphics_root_signature =
|
||||
[](void* user_data, ID3D12RootSignature* root_signature) {
|
||||
static_cast<D3D12CommandProcessor*>(user_data)
|
||||
->GetDeferredCommandList()
|
||||
.D3DSetGraphicsRootSignature(root_signature);
|
||||
};
|
||||
native_context.d3d12.set_graphics_root_32bit_constants =
|
||||
[](void* user_data, UINT root_parameter_index, UINT num_32bit_values_to_set,
|
||||
const void* src_data, UINT dest_offset_in_32bit_values) {
|
||||
static_cast<D3D12CommandProcessor*>(user_data)
|
||||
->GetDeferredCommandList()
|
||||
.D3DSetGraphicsRoot32BitConstants(root_parameter_index, num_32bit_values_to_set,
|
||||
src_data, dest_offset_in_32bit_values);
|
||||
};
|
||||
native_context.d3d12.set_graphics_root_descriptor_table =
|
||||
[](void* user_data, UINT root_parameter_index,
|
||||
D3D12_GPU_DESCRIPTOR_HANDLE base_descriptor) {
|
||||
static_cast<D3D12CommandProcessor*>(user_data)
|
||||
->GetDeferredCommandList()
|
||||
.D3DSetGraphicsRootDescriptorTable(root_parameter_index, base_descriptor);
|
||||
};
|
||||
native_context.d3d12.set_pipeline_state =
|
||||
[](void* user_data, ID3D12PipelineState* pipeline_state) {
|
||||
static_cast<D3D12CommandProcessor*>(user_data)
|
||||
->GetDeferredCommandList()
|
||||
.D3DSetPipelineState(pipeline_state);
|
||||
};
|
||||
native_context.d3d12.ia_set_primitive_topology =
|
||||
[](void* user_data, D3D12_PRIMITIVE_TOPOLOGY primitive_topology) {
|
||||
static_cast<D3D12CommandProcessor*>(user_data)
|
||||
->GetDeferredCommandList()
|
||||
.D3DIASetPrimitiveTopology(primitive_topology);
|
||||
};
|
||||
native_context.d3d12.ia_set_vertex_buffers =
|
||||
[](void* user_data, UINT start_slot, UINT num_views,
|
||||
const D3D12_VERTEX_BUFFER_VIEW* views) {
|
||||
static_cast<D3D12CommandProcessor*>(user_data)
|
||||
->GetDeferredCommandList()
|
||||
.D3DIASetVertexBuffers(start_slot, num_views, views);
|
||||
};
|
||||
native_context.d3d12.om_set_render_targets =
|
||||
[](void* user_data, UINT num_render_target_descriptors,
|
||||
const D3D12_CPU_DESCRIPTOR_HANDLE* render_target_descriptors,
|
||||
BOOL rts_single_handle_to_descriptor_range,
|
||||
const D3D12_CPU_DESCRIPTOR_HANDLE* depth_stencil_descriptor) {
|
||||
static_cast<D3D12CommandProcessor*>(user_data)
|
||||
->GetDeferredCommandList()
|
||||
.D3DOMSetRenderTargets(num_render_target_descriptors, render_target_descriptors,
|
||||
rts_single_handle_to_descriptor_range,
|
||||
depth_stencil_descriptor);
|
||||
};
|
||||
native_context.d3d12.rs_set_viewport = [](void* user_data,
|
||||
const D3D12_VIEWPORT* viewport) {
|
||||
static_cast<D3D12CommandProcessor*>(user_data)
|
||||
->GetDeferredCommandList()
|
||||
.RSSetViewport(*viewport);
|
||||
};
|
||||
native_context.d3d12.rs_set_scissor_rect = [](void* user_data, const D3D12_RECT* rect) {
|
||||
static_cast<D3D12CommandProcessor*>(user_data)->GetDeferredCommandList().RSSetScissorRect(
|
||||
*rect);
|
||||
};
|
||||
native_context.d3d12.draw_instanced =
|
||||
[](void* user_data, UINT vertex_count_per_instance, UINT instance_count,
|
||||
UINT start_vertex_location, UINT start_instance_location) {
|
||||
static_cast<D3D12CommandProcessor*>(user_data)
|
||||
->GetDeferredCommandList()
|
||||
.D3DDrawInstanced(vertex_count_per_instance, instance_count, start_vertex_location,
|
||||
start_instance_location);
|
||||
};
|
||||
if (TryRenderNativeGuestOutput(native_context)) {
|
||||
EndSubmission(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Upload the new gamma ramp, using the upload buffer for the current
|
||||
|
|
@ -2943,6 +2782,14 @@ bool D3D12CommandProcessor::IssueDraw(xenos::PrimitiveType primitive_type, uint3
|
|||
if (ShouldSuppressPassAtPitch(pitch)) {
|
||||
return true;
|
||||
}
|
||||
// Depth/stencil-only draws (no pixel shader) inside the EXEMPT passes:
|
||||
// shadow-map casters and z-prepasses whose output feeds only the
|
||||
// suppressed scene passes (the native renderer builds its own shadows).
|
||||
// Mirrors the Vulkan gate, where this stream was the dominant remaining
|
||||
// emulated GPU cost (the bimodal-FPS slow state).
|
||||
if (pixel_shader == nullptr && ShouldSuppressExemptDepthOnlyDraws()) {
|
||||
return true;
|
||||
}
|
||||
// Census of the passes still EXECUTING under suppression (each distinct
|
||||
// surface pitch logged once): the data for tightening the filter; these
|
||||
// passes pace the whole pipeline (the ~160 fps ceiling was the game's
|
||||
|
|
|
|||
1147
src/graphics/d3d12/native_rhi_d3d12.cpp
Normal file
1147
src/graphics/d3d12/native_rhi_d3d12.cpp
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -14,6 +14,39 @@ REXCVAR_DEFINE_BOOL(native_render_suppress_emulated_draws, false, "GPU",
|
|||
"normally.")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
|
||||
REXCVAR_DEFINE_INT32(native_render_suppress_mode, 2, "GPU",
|
||||
"Which emulated passes to suppress while the native guest-output "
|
||||
"renderer is active. 0 = framebuffer-sized passes only (surface "
|
||||
"pitch >= 1280); the game's whole postfx chain then still executes "
|
||||
"at 1152x640 x resolution scale EVERY frame and paces the pipeline "
|
||||
"(~half the achievable frame rate). 1 = suppress everything (perf "
|
||||
"probing; mid-gameplay lightmap page composition breaks). 2 = "
|
||||
"suppress everything EXCEPT the memory-composition passes whose "
|
||||
"outputs the native renderer samples from guest memory: lightmap "
|
||||
"page composition (pitch 1024) and small composite surfaces (pitch "
|
||||
"<= 512, CAS outfit pieces). Menus/pause/loading always render "
|
||||
"fully (the native renderer yields there), so shop/outfit "
|
||||
"composition is unaffected by any mode. 3 = portrait-window mode: "
|
||||
"like 0 the sub-framebuffer RTT passes execute (one-shot frontend "
|
||||
"portrait renders, census pitches 560-1200), but the 1152-wide "
|
||||
"main scene + postfx band stays suppressed; that band is the "
|
||||
"whole-pipeline cost at scaled resolutions and portraits never "
|
||||
"need it.")
|
||||
.range(0, 3)
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
|
||||
REXCVAR_DEFINE_BOOL(native_render_suppress_exempt_depth_only, true, "GPU",
|
||||
"Within the suppression-EXEMPT passes (see "
|
||||
"native_render_suppress_mode), also skip draws with no pixel shader "
|
||||
"(depth/stencil-only: shadow-map casters, z-prepasses). Their output "
|
||||
"feeds only the suppressed scene passes; the native renderer builds "
|
||||
"its own shadows, and on Vulkan this stream is the dominant "
|
||||
"remaining emulated GPU cost (2-12 ms/frame at 3x, the bimodal-FPS "
|
||||
"slow state). Disable if lightmap-page or CAS composition content "
|
||||
"regresses (a composition pass depth/stencil-testing against its own "
|
||||
"no-PS lay-down would need this off).")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
|
||||
namespace rex::graphics {
|
||||
namespace {
|
||||
|
||||
|
|
@ -42,6 +75,10 @@ bool TryRenderNativeGuestOutput(const NativeGuestOutputRenderContext& context) {
|
|||
return rendered;
|
||||
}
|
||||
|
||||
bool HasNativeGuestOutputRenderer() {
|
||||
return g_renderer.load(std::memory_order_acquire) != nullptr;
|
||||
}
|
||||
|
||||
bool IsNativeGuestOutputActive() {
|
||||
return g_native_output_active.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
|
@ -51,4 +88,31 @@ bool ShouldSuppressEmulatedDraws() {
|
|||
g_native_output_active.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool ShouldSuppressExemptDepthOnlyDraws() {
|
||||
return REXCVAR_GET(native_render_suppress_exempt_depth_only);
|
||||
}
|
||||
|
||||
bool ShouldSuppressPassAtPitch(uint32_t surface_pitch) {
|
||||
switch (REXCVAR_GET(native_render_suppress_mode)) {
|
||||
case 0:
|
||||
return surface_pitch >= 1280;
|
||||
case 1:
|
||||
return true;
|
||||
case 3:
|
||||
// Portrait-window mode: the one-shot frontend portrait RTTs (Skate 3
|
||||
// census during the window: 1200/800/640/600/560 + small mips)
|
||||
// execute, while the 1152-wide main scene/postfx band, the
|
||||
// whole-pipeline cost at scaled resolutions, stays suppressed like
|
||||
// the framebuffer.
|
||||
return surface_pitch >= 1280 || surface_pitch == 1152;
|
||||
default:
|
||||
// Execute only the memory-composition passes the native renderer
|
||||
// samples: lightmap page composition (1024) and small composite
|
||||
// surfaces (<= 512, CAS outfit pieces). The <= 512 window also lets a
|
||||
// few tiny postfx pyramid mips through, negligible, and safer than
|
||||
// guessing which small surfaces matter.
|
||||
return !(surface_pitch == 1024 || surface_pitch <= 512);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace rex::graphics
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
|
|
@ -29,14 +30,17 @@
|
|||
#include <SPIRV/GlslangToSpv.h>
|
||||
#include <glslang/Public/ShaderLang.h>
|
||||
#include <rex/assert.h>
|
||||
#include <rex/chrono/clock.h>
|
||||
#include <rex/cvar.h>
|
||||
#include <rex/dbg.h>
|
||||
#include <rex/perf/counter.h>
|
||||
#include <rex/logging.h>
|
||||
#include <rex/math.h>
|
||||
#include <rex/platform.h>
|
||||
#include <rex/graphics/native_guest_renderer.h>
|
||||
#include <rex/graphics/util/draw.h>
|
||||
#include <rex/graphics/flags.h>
|
||||
#include <rex/graphics/vulkan/native_rhi_vulkan.h>
|
||||
#include <rex/graphics/pipeline/shader/shader.h>
|
||||
#include <rex/graphics/pipeline/shader/spirv_translator.h>
|
||||
#include <rex/graphics/registers.h>
|
||||
|
|
@ -1029,6 +1033,49 @@ void VulkanCommandProcessor::RestoreEdramSnapshot(const void* snapshot) {
|
|||
|
||||
bool VulkanCommandProcessor::ExecutePacketType3_EVENT_WRITE_ZPD(memory::RingBuffer* reader,
|
||||
uint32_t packet, uint32_t count) {
|
||||
// Native guest-output renderer active: the framebuffer-sized draws between
|
||||
// query begin/end are suppressed, so a real host occlusion query reports 0
|
||||
// samples. The game gates world rendering on these results - event-ad
|
||||
// placements poll their poster quads' visibility and stop submitting the
|
||||
// overlay geometry entirely (the native frame then shows the default poster
|
||||
// art where the emulated frame shows the current ad). Report the fake
|
||||
// positive count instead: the suppressed draws are exactly the ones the
|
||||
// native renderer is drawing in their place. (Mirrors the D3D12 gate.)
|
||||
if (ShouldSuppressEmulatedDraws()) {
|
||||
if (active_occlusion_query_.valid) {
|
||||
// End the host query cleanly so its pool slot is not left active.
|
||||
uint32_t host_index = active_occlusion_query_.host_index;
|
||||
active_occlusion_query_ = {};
|
||||
if (occlusion_query_pool_ != VK_NULL_HANDLE && BeginSubmission(true)) {
|
||||
EndRenderPass();
|
||||
deferred_command_buffer_.CmdVkEndQuery(occlusion_query_pool_, host_index);
|
||||
EndSubmission(false);
|
||||
}
|
||||
}
|
||||
// Set by D3D as BE but struct ABI is LE.
|
||||
const uint32_t kQueryFinished = rex::byte_swap(0xFFFFFEED);
|
||||
assert_true(count == 1);
|
||||
uint32_t initiator = reader->ReadAndSwap<uint32_t>();
|
||||
WriteRegister(XE_GPU_REG_VGT_EVENT_INITIATOR, initiator & 0x3F);
|
||||
uint32_t sample_count_addr = register_file_->values[XE_GPU_REG_RB_SAMPLE_COUNT_ADDR];
|
||||
auto* sample_counts =
|
||||
memory_->TranslatePhysical<xenos::xe_gpu_depth_sample_counts*>(sample_count_addr);
|
||||
if (!sample_counts) {
|
||||
return true;
|
||||
}
|
||||
int32_t fake_sample_count = REXCVAR_GET(query_occlusion_fake_sample_count);
|
||||
bool is_end_via_z_pass =
|
||||
sample_counts->ZPass_A == kQueryFinished && sample_counts->ZPass_B == kQueryFinished;
|
||||
bool is_end_via_z_fail =
|
||||
sample_counts->ZFail_A == kQueryFinished && sample_counts->ZFail_B == kQueryFinished;
|
||||
std::memset(sample_counts, 0, sizeof(xenos::xe_gpu_depth_sample_counts));
|
||||
if (is_end_via_z_pass || is_end_via_z_fail) {
|
||||
sample_counts->ZPass_A = fake_sample_count;
|
||||
sample_counts->Total_A = fake_sample_count;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!REXCVAR_GET(occlusion_query_enable) || !occlusion_query_resources_available_) {
|
||||
return CommandProcessor::ExecutePacketType3_EVENT_WRITE_ZPD(reader, packet, count);
|
||||
}
|
||||
|
|
@ -2302,6 +2349,11 @@ void VulkanCommandProcessor::ShutdownContext() {
|
|||
ShutdownGpuTimestampResources();
|
||||
ShutdownOcclusionQueryResources();
|
||||
|
||||
if (native_rhi_device_ != nullptr) {
|
||||
DestroyNativeRhiDevice(native_rhi_device_);
|
||||
native_rhi_device_ = nullptr;
|
||||
}
|
||||
|
||||
const ui::vulkan::VulkanDevice* const vulkan_device = GetVulkanDevice();
|
||||
const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions();
|
||||
const VkDevice device = vulkan_device->device();
|
||||
|
|
@ -2871,8 +2923,8 @@ void VulkanCommandProcessor::IssueSwap(uint32_t frontbuffer_ptr, uint32_t frontb
|
|||
|
||||
presenter->RefreshGuestOutput(
|
||||
guest_output_width, guest_output_height, display_width, display_height,
|
||||
[this, guest_output_width, guest_output_height, frontbuffer_format, swap_texture_view,
|
||||
swap_post_effect,
|
||||
[this, guest_output_width, guest_output_height, display_width, display_height,
|
||||
frontbuffer_format, swap_texture_view, swap_post_effect,
|
||||
swap_source_needs_rb_swap](ui::Presenter::GuestOutputRefreshContext& context) -> bool {
|
||||
// In case the swap command is the only one in the frame.
|
||||
if (!BeginSubmission(true)) {
|
||||
|
|
@ -2900,6 +2952,49 @@ void VulkanCommandProcessor::IssueSwap(uint32_t frontbuffer_ptr, uint32_t frontb
|
|||
bool use_pwl_gamma_ramp =
|
||||
frontbuffer_format == xenos::TextureFormat::k_2_10_10_10 ||
|
||||
frontbuffer_format == xenos::TextureFormat::k_2_10_10_10_AS_16_16_16_16;
|
||||
|
||||
// Native guest-output renderer hook - mirrors the D3D12 refresher
|
||||
// block: right after the backend refresh context is obtained,
|
||||
// before any gamma-ramp/FXAA work. On success the whole gamma pass
|
||||
// is skipped and the presenter's paint path consumes the natively
|
||||
// rendered guest image unchanged. Set the 8bpc flag the way the
|
||||
// D3D12 hook sees it (the emulated decision for the non-FXAA case);
|
||||
// the emulated path below overwrites it if the renderer yields.
|
||||
if (HasNativeGuestOutputRenderer()) {
|
||||
context.SetIs8bpc(!use_pwl_gamma_ramp && !use_fxaa);
|
||||
NativeGuestOutputRenderContext native_context;
|
||||
native_context.backend = NativeGuestOutputBackend::kVulkan;
|
||||
native_context.guest_output_width = guest_output_width;
|
||||
native_context.guest_output_height = guest_output_height;
|
||||
native_context.display_width = display_width;
|
||||
native_context.display_height = display_height;
|
||||
if (native_rhi_device_ == nullptr) {
|
||||
native_rhi_device_ = CreateNativeRhiDevice(this);
|
||||
}
|
||||
native_context.device = native_rhi_device_;
|
||||
native_context.cmd = NativeRhiBeginFrame(
|
||||
native_rhi_device_, vulkan_context.image(), vulkan_context.image_view(),
|
||||
vulkan_context.image_ever_written_previously(), guest_output_width,
|
||||
guest_output_height, &native_context.guest_output);
|
||||
// Attribute the native pass's GPU time to its own profile bucket
|
||||
// (it otherwise smears into the last emulated draw's bucket).
|
||||
BeginGpuTimestampedRegion(rex::perf::DrawBucket::kNativeScene);
|
||||
if (TryRenderNativeGuestOutput(native_context)) {
|
||||
NativeRhiEndFrame(native_rhi_device_);
|
||||
// Need to submit all the commands before giving the image back
|
||||
// to the presenter (it submits its own for displaying it), and
|
||||
// the release barrier to the presenter's internal state must be
|
||||
// recorded.
|
||||
EndSubmission(true);
|
||||
return true;
|
||||
}
|
||||
// The renderer yielded to the emulated path: close anything the
|
||||
// callback may have left open (render pass, pending clears) -
|
||||
// the gamma pass below records into the same deferred command
|
||||
// buffer.
|
||||
NativeRhiEndFrame(native_rhi_device_);
|
||||
}
|
||||
|
||||
bool swap_source_requires_compute_rb_swap =
|
||||
!vulkan_device->properties().imageViewFormatSwizzle && swap_source_needs_rb_swap;
|
||||
auto select_swap_apply_gamma_compute_pipeline = [&](bool use_pwl,
|
||||
|
|
@ -4118,6 +4213,42 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t
|
|||
fingerprint.primitive_count = primitive_count;
|
||||
PROFILE_DRAW_FINGERPRINT(fingerprint);
|
||||
}
|
||||
if (ShouldSuppressEmulatedDraws() &&
|
||||
ShouldSuppressPassAtPitch(regs.Get<reg::RB_SURFACE_INFO>().surface_pitch)) {
|
||||
// Native output active: skip the resolves of SUPPRESSED passes - their
|
||||
// draws left garbage in EDRAM, and copying it out would overwrite
|
||||
// guest texture payloads the native renderer still samples. Passes
|
||||
// that EXECUTE under the current suppression mode (lightmap page
|
||||
// composition, CAS composites) must keep their resolves: skipping
|
||||
// lightpage composition left never-composed pages sampling garbage -
|
||||
// the light/dark checkerboard ground. (Mirrors the D3D12 gate: draw
|
||||
// and resolve suppression must agree.)
|
||||
if (REXCVAR_GET(native_render_force_resolve_readback_max_length) > 0) {
|
||||
// App-armed window diagnostics (Skate 3 photo flows): while the
|
||||
// window is armed, show which resolves the suppression filter drops
|
||||
// - pins passes (e.g. the photo display-card compose) whose output
|
||||
// never reaches guest memory. Throttled.
|
||||
static uint64_t s_drop_log_ms = 0;
|
||||
static uint32_t s_drop_log_count = 0;
|
||||
const uint64_t now_ms = rex::chrono::Clock::QueryHostUptimeMillis();
|
||||
if (now_ms - s_drop_log_ms >= 1000) {
|
||||
if (s_drop_log_count > 8) {
|
||||
REXLOG_INFO("readback-window: (+{} more dropped resolves)",
|
||||
s_drop_log_count - 8);
|
||||
}
|
||||
s_drop_log_ms = now_ms;
|
||||
s_drop_log_count = 0;
|
||||
}
|
||||
if (++s_drop_log_count <= 8) {
|
||||
REXLOG_INFO(
|
||||
"readback-window: resolve DROPPED with suppressed pass "
|
||||
"(surface_pitch={} copy_dest=0x{:08X})",
|
||||
regs.Get<reg::RB_SURFACE_INFO>().surface_pitch,
|
||||
uint32_t(regs[XE_GPU_REG_RB_COPY_DEST_BASE]));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
uint32_t gpu_timestamp_start =
|
||||
BeginGpuTimestampedDraw(rex::perf::DrawBucket::kCopyResolve);
|
||||
bool copy_result = IssueCopy();
|
||||
|
|
@ -4213,6 +4344,48 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t
|
|||
VulkanShader::VulkanTranslation* pixel_shader_translation;
|
||||
bool memexport_writes_possible = memexport_used_vertex || memexport_used_pixel;
|
||||
bool draw_samplers_reused = false;
|
||||
|
||||
// Native guest-output renderer active: the emulated frame is never shown,
|
||||
// so skip the draw entirely (pipeline setup, texture cache, render target
|
||||
// cache, GPU work). Memexport draws still execute - the game reads their
|
||||
// results back from memory. Fences, queries and PM4 processing are
|
||||
// unaffected. Menus/pause flip the native renderer inactive, so emulated
|
||||
// rendering works normally there.
|
||||
//
|
||||
// Only FRAMEBUFFER-SIZED passes (surface pitch >= 1280: main pass,
|
||||
// z-prepass, HUD overlay RTT) are suppressed. Passes into smaller
|
||||
// surfaces keep executing: lightmap PAGE COMPOSITION renders into
|
||||
// 1024-wide pages whose CPU payloads the native renderer samples -
|
||||
// suppressing it left pages that streamed in during native play
|
||||
// uncomposed (garbage), the light/dark checkerboard ground. CAS outfit
|
||||
// composition mid-gameplay is covered by the same rule. (Mirrors the
|
||||
// D3D12 gate; agreement with the resolve gate above is an invariant.)
|
||||
if (!memexport_writes_possible && ShouldSuppressEmulatedDraws()) {
|
||||
const uint32_t suppress_pitch = regs.Get<reg::RB_SURFACE_INFO>().surface_pitch;
|
||||
if (ShouldSuppressPassAtPitch(suppress_pitch)) {
|
||||
return true;
|
||||
}
|
||||
// Depth/stencil-only draws (no pixel shader) inside the EXEMPT passes:
|
||||
// shadow-map casters and z-prepasses whose output feeds only the
|
||||
// suppressed scene passes (the native renderer builds its own shadows).
|
||||
// This stream is the dominant remaining emulated GPU cost at 3x on
|
||||
// Vulkan (2-12 ms/frame - the bimodal-FPS slow state).
|
||||
if (pixel_shader == nullptr && ShouldSuppressExemptDepthOnlyDraws()) {
|
||||
return true;
|
||||
}
|
||||
// Census of the passes still EXECUTING under suppression (each distinct
|
||||
// surface pitch logged once): the data for tightening the filter - these
|
||||
// passes pace the whole pipeline (gameplay census on Skate 3: pitches
|
||||
// 1200/800/640/600/560/320/280/80).
|
||||
static std::atomic<uint32_t> logged_pitch_mask[8192 / 32] = {};
|
||||
if (suppress_pitch < 8192) {
|
||||
const uint32_t bit = 1u << (suppress_pitch % 32);
|
||||
if ((logged_pitch_mask[suppress_pitch / 32].fetch_or(bit) & bit) == 0) {
|
||||
REXGPU_INFO("suppression census: executing draws at surface_pitch={}", suppress_pitch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
analyze_stage_timer.Stop();
|
||||
|
||||
// Two iterations because a submission (even the current one - in which case
|
||||
|
|
@ -5265,7 +5438,7 @@ bool VulkanCommandProcessor::DownscaleScaledResolveToSharedMemory(uint32_t start
|
|||
uint64_t scaled_length = 0;
|
||||
if (!texture_cache_->GetScaledResolveRange(start_unscaled, length_unscaled, 0, scaled_start,
|
||||
scaled_length) ||
|
||||
!scaled_length || scaled_start > uint64_t(UINT32_MAX)) {
|
||||
!scaled_length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -5383,10 +5556,25 @@ bool VulkanCommandProcessor::DownscaleScaledResolveToSharedMemory(uint32_t start
|
|||
return false;
|
||||
}
|
||||
|
||||
// Bind the source as a bounded sub-range of the scaled resolve buffer, not
|
||||
// the whole buffer: at 3x3 scaling the buffer is kBufferSize * 9 = 4.5 GB,
|
||||
// and a storage buffer descriptor of that size exceeds maxStorageBufferRange
|
||||
// (at most 4 GB - 1) - drivers' robust-access bound then wraps to
|
||||
// (4.5 GB mod 4 GB) = 512 MB, so the downscale compute read ZEROS for every
|
||||
// scaled address >= 0x20000000 (proven by the Skate 3 photo-grab readback
|
||||
// window: only the one resolve whose scaled range straddled 0x20000000
|
||||
// delivered content, everything above was 100% zeros). Mirrors the D3D12
|
||||
// path, which creates the source SRV at the resolve's own offset with a
|
||||
// small range; the shader receives only the intra-binding residual.
|
||||
const VkDeviceSize source_offset_alignment = std::max(
|
||||
vulkan_device->properties().minStorageBufferOffsetAlignment, VkDeviceSize(4));
|
||||
const VkDeviceSize source_binding_offset =
|
||||
VkDeviceSize(scaled_start) & ~(source_offset_alignment - 1);
|
||||
const uint32_t source_offset_residual = uint32_t(scaled_start - source_binding_offset);
|
||||
VkDescriptorBufferInfo buffer_infos[2] = {};
|
||||
buffer_infos[0].buffer = scaled_resolve_buffer;
|
||||
buffer_infos[0].offset = 0;
|
||||
buffer_infos[0].range = VK_WHOLE_SIZE;
|
||||
buffer_infos[0].offset = source_binding_offset;
|
||||
buffer_infos[0].range = VkDeviceSize(source_offset_residual) + scaled_length;
|
||||
buffer_infos[1].buffer = resolve_downscale_buffer_;
|
||||
buffer_infos[1].offset = 0;
|
||||
buffer_infos[1].range = length_unscaled;
|
||||
|
|
@ -5428,7 +5616,8 @@ bool VulkanCommandProcessor::DownscaleScaledResolveToSharedMemory(uint32_t start
|
|||
constants.scale_y = texture_cache_->draw_resolution_scale_y();
|
||||
constants.pixel_size_log2 = pixel_size_log2;
|
||||
constants.tile_count = tile_count;
|
||||
constants.source_offset_bytes = uint32_t(scaled_start);
|
||||
// Relative to the descriptor's binding offset (see buffer_infos[0] above).
|
||||
constants.source_offset_bytes = source_offset_residual;
|
||||
constants.half_pixel_offset =
|
||||
(REXCVAR_GET(readback_resolve_half_pixel_offset) &&
|
||||
(constants.scale_x > 1 || constants.scale_y > 1))
|
||||
|
|
@ -5576,6 +5765,14 @@ bool VulkanCommandProcessor::IssueCopy_ReadbackResolvePath() {
|
|||
readback_mode, false, true,
|
||||
force_scaled_resolve_cpu_copy ? kImportSkaterPreviewResolveLength
|
||||
: uint32_t(force_readback_max_length));
|
||||
// Unconditional (the debug counters are boot-zeroed by the app): the
|
||||
// app-armed shutter-burst window is ~1.5 s and the photo display card
|
||||
// composes from exactly these CPU copies; visibility here is the whole
|
||||
// diagnosis when the card is wrong (mirrors the D3D12 readback-window
|
||||
// log).
|
||||
REXLOG_INFO("readback-window: resolve dest=0x{:08X} len=0x{:X} -> CPU copy (kFull{})",
|
||||
written_address, written_length,
|
||||
force_scaled_resolve_cpu_copy ? ", targeted" : ", forced window");
|
||||
}
|
||||
|
||||
const int32_t max_readback_length = REXCVAR_GET(vulkan_readback_resolve_max_length);
|
||||
|
|
@ -5701,7 +5898,7 @@ bool VulkanCommandProcessor::IssueCopy_ReadbackResolvePath() {
|
|||
}
|
||||
|
||||
VkBuffer scaled_resolve_buffer = texture_cache_->scaled_resolve_buffer();
|
||||
if (scaled_resolve_buffer == VK_NULL_HANDLE || scaled_start > uint64_t(UINT32_MAX)) {
|
||||
if (scaled_resolve_buffer == VK_NULL_HANDLE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -5745,10 +5942,27 @@ bool VulkanCommandProcessor::IssueCopy_ReadbackResolvePath() {
|
|||
return true;
|
||||
}
|
||||
|
||||
// Bind the source as a bounded sub-range of the scaled resolve buffer,
|
||||
// not the whole buffer: at 3x3 scaling the buffer is kBufferSize * 9 =
|
||||
// 4.5 GB, and a storage buffer descriptor of that size exceeds
|
||||
// maxStorageBufferRange (at most 4 GB - 1) - drivers' robust-access bound
|
||||
// then wraps to (4.5 GB mod 4 GB) = 512 MB, so the downscale compute read
|
||||
// ZEROS for every scaled address >= 0x20000000. This is what broke the
|
||||
// Skate 3 photo-grab readback window at scale 3 (skate3_1525: dest
|
||||
// 0x03855000, scaled 0x1FAFD000, delivered content with exactly the
|
||||
// above-boundary 44% tail zeroed; every dest whose scaled range sat
|
||||
// entirely above 0x20000000 read 100% zeros). Mirrors the D3D12 path,
|
||||
// which creates the source SRV at the resolve's own offset with a small
|
||||
// range; the shader receives only the intra-binding residual.
|
||||
const VkDeviceSize source_offset_alignment = std::max(
|
||||
vulkan_device->properties().minStorageBufferOffsetAlignment, VkDeviceSize(4));
|
||||
const VkDeviceSize source_binding_offset =
|
||||
VkDeviceSize(scaled_start) & ~(source_offset_alignment - 1);
|
||||
const uint32_t source_offset_residual = uint32_t(scaled_start - source_binding_offset);
|
||||
VkDescriptorBufferInfo buffer_infos[2] = {};
|
||||
buffer_infos[0].buffer = scaled_resolve_buffer;
|
||||
buffer_infos[0].offset = 0;
|
||||
buffer_infos[0].range = VK_WHOLE_SIZE;
|
||||
buffer_infos[0].offset = source_binding_offset;
|
||||
buffer_infos[0].range = VkDeviceSize(source_offset_residual) + scaled_length;
|
||||
buffer_infos[1].buffer = resolve_downscale_buffer_;
|
||||
buffer_infos[1].offset = 0;
|
||||
buffer_infos[1].range = written_length;
|
||||
|
|
@ -5790,7 +6004,8 @@ bool VulkanCommandProcessor::IssueCopy_ReadbackResolvePath() {
|
|||
constants.scale_y = texture_cache_->draw_resolution_scale_y();
|
||||
constants.pixel_size_log2 = pixel_size_log2;
|
||||
constants.tile_count = tile_count;
|
||||
constants.source_offset_bytes = uint32_t(scaled_start);
|
||||
// Relative to the descriptor's binding offset (see buffer_infos[0] above).
|
||||
constants.source_offset_bytes = source_offset_residual;
|
||||
constants.half_pixel_offset = (REXCVAR_GET(readback_resolve_half_pixel_offset) &&
|
||||
(constants.scale_x > 1 || constants.scale_y > 1))
|
||||
? 1u
|
||||
|
|
@ -5882,6 +6097,24 @@ bool VulkanCommandProcessor::IssueCopy_ReadbackResolvePath() {
|
|||
DebugDumpResolveReadback(written_address, written_length, is_scaled,
|
||||
readback.mapped_data[read_index]);
|
||||
}
|
||||
if (force_window_cpu_copy || force_scaled_resolve_cpu_copy) {
|
||||
// App-armed window content probe (the photo display card composes
|
||||
// from exactly these bytes): strided sample of hash + zero fraction
|
||||
// so a broken scaled-readback path is visible in the log.
|
||||
const uint32_t* words = reinterpret_cast<const uint32_t*>(readback.mapped_data[read_index]);
|
||||
const uint32_t word_count = written_length >> 2;
|
||||
uint64_t hash = 1469598103934665603ull;
|
||||
uint32_t zero_words = 0;
|
||||
const uint32_t samples = std::min(word_count, 4096u);
|
||||
for (uint32_t i = 0; i < samples; ++i) {
|
||||
const uint32_t w = words[uint64_t(word_count - 1) * i / std::max(samples - 1, 1u)];
|
||||
hash = (hash ^ w) * 1099511628211ull;
|
||||
zero_words += w == 0 ? 1 : 0;
|
||||
}
|
||||
REXLOG_INFO("readback-window: content dest=0x{:08X} len=0x{:X} hash={:016X} zero={}%",
|
||||
written_address, written_length, hash,
|
||||
samples != 0 ? zero_words * 100 / samples : 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -6242,6 +6475,7 @@ void VulkanCommandProcessor::ProcessGpuTimestampResults() {
|
|||
}
|
||||
if (size_t(frame.buckets[i]) < size_t(rex::perf::DrawBucket::kCount)) {
|
||||
gpu_profile_bucket_us_[size_t(frame.buckets[i])] += uint64_t(elapsed_us);
|
||||
++gpu_profile_bucket_n_[size_t(frame.buckets[i])];
|
||||
}
|
||||
rex::perf::CounterId counter_id;
|
||||
switch (frame.buckets[i]) {
|
||||
|
|
@ -6314,8 +6548,10 @@ void VulkanCommandProcessor::ProcessGpuTimestampResults() {
|
|||
if (!total_us) {
|
||||
continue;
|
||||
}
|
||||
breakdown += fmt::format(" {}={}", rex::perf::DrawBucketName(rex::perf::DrawBucket(bucket)),
|
||||
total_us / gpu_profile_frames_);
|
||||
breakdown += fmt::format(" {}={}({})",
|
||||
rex::perf::DrawBucketName(rex::perf::DrawBucket(bucket)),
|
||||
total_us / gpu_profile_frames_,
|
||||
gpu_profile_bucket_n_[bucket] / gpu_profile_frames_);
|
||||
}
|
||||
uint64_t span_avg_us = gpu_profile_span_us_ / gpu_profile_frames_;
|
||||
uint64_t sum_avg_us = buckets_sum_us / gpu_profile_frames_;
|
||||
|
|
@ -6327,6 +6563,7 @@ void VulkanCommandProcessor::ProcessGpuTimestampResults() {
|
|||
gpu_profile_submissions_ / gpu_profile_frames_, frame.query_count,
|
||||
kMaxGpuTimestampQueriesPerFrame, breakdown);
|
||||
std::memset(gpu_profile_bucket_us_, 0, sizeof(gpu_profile_bucket_us_));
|
||||
std::memset(gpu_profile_bucket_n_, 0, sizeof(gpu_profile_bucket_n_));
|
||||
gpu_profile_span_us_ = 0;
|
||||
gpu_profile_submissions_ = 0;
|
||||
gpu_profile_frames_ = 0;
|
||||
|
|
|
|||
|
|
@ -167,6 +167,16 @@ void DeferredCommandBuffer::Execute(VkCommandBuffer command_buffer) {
|
|||
rex::align(sizeof(ArgsVkCopyBufferToImage), alignof(VkBufferImageCopy))));
|
||||
} break;
|
||||
|
||||
case Command::kVkCopyImageToBuffer: {
|
||||
auto& args = *reinterpret_cast<const ArgsVkCopyImageToBuffer*>(stream);
|
||||
dfn.vkCmdCopyImageToBuffer(
|
||||
command_buffer, args.src_image, args.src_image_layout, args.dst_buffer,
|
||||
args.region_count,
|
||||
reinterpret_cast<const VkBufferImageCopy*>(
|
||||
reinterpret_cast<const uint8_t*>(stream) +
|
||||
rex::align(sizeof(ArgsVkCopyImageToBuffer), alignof(VkBufferImageCopy))));
|
||||
} break;
|
||||
|
||||
case Command::kVkCopyQueryPoolResults: {
|
||||
auto& args = *reinterpret_cast<const ArgsVkCopyQueryPoolResults*>(stream);
|
||||
dfn.vkCmdCopyQueryPoolResults(command_buffer, args.query_pool, args.first_query,
|
||||
|
|
|
|||
2585
src/graphics/vulkan/native_rhi_vulkan.cpp
Normal file
2585
src/graphics/vulkan/native_rhi_vulkan.cpp
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue