GPU: native RHI abstraction (nrhi) + Vulkan backend for the native renderer

This commit is contained in:
mchughalex 2026-07-16 22:33:39 +01:00
parent 065cade4d3
commit da06b7a51b
16 changed files with 4768 additions and 258 deletions

View file

@ -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];
};

View 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

View file

@ -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

View 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

View file

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

View file

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

View 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

View file

@ -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";
}

View file

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

View file

@ -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()

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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

View file

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

View file

@ -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,

File diff suppressed because it is too large Load diff