From da06b7a51b80f7e41ea12c1e4e8ecd5690ffe738 Mon Sep 17 00:00:00 2001 From: mchughalex <25671999+mchughalex@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:33:39 +0100 Subject: [PATCH] GPU: native RHI abstraction (nrhi) + Vulkan backend for the native renderer --- .../rex/graphics/d3d12/command_processor.h | 8 + include/rex/graphics/d3d12/native_rhi_d3d12.h | 33 + include/rex/graphics/native_guest_renderer.h | 94 +- include/rex/graphics/native_rhi.h | 510 ++++ .../rex/graphics/vulkan/command_processor.h | 11 + .../graphics/vulkan/deferred_command_buffer.h | 30 + .../rex/graphics/vulkan/native_rhi_vulkan.h | 45 + include/rex/perf/counter.h | 5 + .../rex/ui/vulkan/functions/device_1_0.inc | 1 + src/graphics/CMakeLists.txt | 3 + src/graphics/d3d12/command_processor.cpp | 219 +- src/graphics/d3d12/native_rhi_d3d12.cpp | 1147 ++++++++ src/graphics/native_guest_renderer.cpp | 64 + src/graphics/vulkan/command_processor.cpp | 261 +- .../vulkan/deferred_command_buffer.cpp | 10 + src/graphics/vulkan/native_rhi_vulkan.cpp | 2585 +++++++++++++++++ 16 files changed, 4768 insertions(+), 258 deletions(-) create mode 100644 include/rex/graphics/d3d12/native_rhi_d3d12.h create mode 100644 include/rex/graphics/native_rhi.h create mode 100644 include/rex/graphics/vulkan/native_rhi_vulkan.h create mode 100644 src/graphics/d3d12/native_rhi_d3d12.cpp create mode 100644 src/graphics/vulkan/native_rhi_vulkan.cpp diff --git a/include/rex/graphics/d3d12/command_processor.h b/include/rex/graphics/d3d12/command_processor.h index 12dd68c..3f43fad 100644 --- a/include/rex/graphics/d3d12/command_processor.h +++ b/include/rex/graphics/d3d12/command_processor.h @@ -43,6 +43,10 @@ #include #include +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]; }; diff --git a/include/rex/graphics/d3d12/native_rhi_d3d12.h b/include/rex/graphics/d3d12/native_rhi_d3d12.h new file mode 100644 index 0000000..bbbb7cf --- /dev/null +++ b/include/rex/graphics/d3d12/native_rhi_d3d12.h @@ -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 + +#include +#include + +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 diff --git a/include/rex/graphics/native_guest_renderer.h b/include/rex/graphics/native_guest_renderer.h index eebd30a..147101a 100644 --- a/include/rex/graphics/native_guest_renderer.h +++ b/include/rex/graphics/native_guest_renderer.h @@ -2,13 +2,7 @@ #include -#if REX_HAS_D3D12 -#include -#endif - -namespace rex::graphics::d3d12 { -class D3D12CommandProcessor; -} +#include 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 diff --git a/include/rex/graphics/native_rhi.h b/include/rex/graphics/native_rhi.h new file mode 100644 index 0000000..ef04b42 --- /dev/null +++ b/include/rex/graphics/native_rhi.h @@ -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 + +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). 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 diff --git a/include/rex/graphics/vulkan/command_processor.h b/include/rex/graphics/vulkan/command_processor.h index 2a66a46..0cafcfa 100644 --- a/include/rex/graphics/vulkan/command_processor.h +++ b/include/rex/graphics/vulkan/command_processor.h @@ -44,6 +44,10 @@ #include #include +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 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; diff --git a/include/rex/graphics/vulkan/deferred_command_buffer.h b/include/rex/graphics/vulkan/deferred_command_buffer.h index 3e12ade..5aeec6b 100644 --- a/include/rex/graphics/vulkan/deferred_command_buffer.h +++ b/include/rex/graphics/vulkan/deferred_command_buffer.h @@ -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(WriteCommand( + Command::kVkCopyImageToBuffer, header_size + sizeof(VkBufferImageCopy) * region_count)); + auto& args = *reinterpret_cast(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(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; diff --git a/include/rex/graphics/vulkan/native_rhi_vulkan.h b/include/rex/graphics/vulkan/native_rhi_vulkan.h new file mode 100644 index 0000000..d4c99fd --- /dev/null +++ b/include/rex/graphics/vulkan/native_rhi_vulkan.h @@ -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 + +#include +#include + +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 diff --git a/include/rex/perf/counter.h b/include/rex/perf/counter.h index 1064b09..dfa59a9 100644 --- a/include/rex/perf/counter.h +++ b/include/rex/perf/counter.h @@ -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"; } diff --git a/include/rex/ui/vulkan/functions/device_1_0.inc b/include/rex/ui/vulkan/functions/device_1_0.inc index 5b619f9..5f6c915 100644 --- a/include/rex/ui/vulkan/functions/device_1_0.inc +++ b/include/rex/ui/vulkan/functions/device_1_0.inc @@ -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) diff --git a/src/graphics/CMakeLists.txt b/src/graphics/CMakeLists.txt index 43e8e78..2fd7bb2 100644 --- a/src/graphics/CMakeLists.txt +++ b/src/graphics/CMakeLists.txt @@ -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() diff --git a/src/graphics/d3d12/command_processor.cpp b/src/graphics/d3d12/command_processor.cpp index 19a315b..122ae9c 100644 --- a/src/graphics/d3d12/command_processor.cpp +++ b/src/graphics/d3d12/command_processor.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -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(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(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(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(user_data)->PushTransitionBarrier( - resource, old_state, new_state); - }; - native_context.d3d12.submit_barriers = [](void* user_data) { - static_cast(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(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(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(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(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(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(user_data) - ->GetDeferredCommandList() - .D3DSetGraphicsRootDescriptorTable(root_parameter_index, base_descriptor); - }; - native_context.d3d12.set_pipeline_state = - [](void* user_data, ID3D12PipelineState* pipeline_state) { - static_cast(user_data) - ->GetDeferredCommandList() - .D3DSetPipelineState(pipeline_state); - }; - native_context.d3d12.ia_set_primitive_topology = - [](void* user_data, D3D12_PRIMITIVE_TOPOLOGY primitive_topology) { - static_cast(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(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(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(user_data) - ->GetDeferredCommandList() - .RSSetViewport(*viewport); - }; - native_context.d3d12.rs_set_scissor_rect = [](void* user_data, const D3D12_RECT* rect) { - static_cast(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(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 diff --git a/src/graphics/d3d12/native_rhi_d3d12.cpp b/src/graphics/d3d12/native_rhi_d3d12.cpp new file mode 100644 index 0000000..673ebc1 --- /dev/null +++ b/src/graphics/d3d12/native_rhi_d3d12.cpp @@ -0,0 +1,1147 @@ +// D3D12 implementation of the native-render RHI. A thin passthrough onto the +// D3D12 command processor's deferred command list, barrier queue and +// submission counters; the descriptor-field choices here reproduce exactly +// what the scene renderer set when it talked to D3D12 directly, so migrating +// the scene onto the RHI is behavior-preserving on this backend. + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace rex::graphics::d3d12 { +namespace { + +using nrhi::Backend; +using nrhi::Format; +using nrhi::HeapKind; +using nrhi::ResourceState; +using nrhi::TextureKind; + +DXGI_FORMAT ToDxgi(Format format) { + switch (format) { + case Format::kR8G8B8A8_UNORM: + return DXGI_FORMAT_R8G8B8A8_UNORM; + case Format::kR8G8B8A8_UINT: + return DXGI_FORMAT_R8G8B8A8_UINT; + case Format::kR8_UNORM: + return DXGI_FORMAT_R8_UNORM; + case Format::kR8G8_UNORM: + return DXGI_FORMAT_R8G8_UNORM; + case Format::kB5G6R5_UNORM: + return DXGI_FORMAT_B5G6R5_UNORM; + case Format::kR16G16_UNORM: + return DXGI_FORMAT_R16G16_UNORM; + case Format::kR10G10B10A2_UNORM: + return DXGI_FORMAT_R10G10B10A2_UNORM; + case Format::kR32_FLOAT: + return DXGI_FORMAT_R32_FLOAT; + case Format::kR32G32_FLOAT: + return DXGI_FORMAT_R32G32_FLOAT; + case Format::kR32G32B32_FLOAT: + return DXGI_FORMAT_R32G32B32_FLOAT; + case Format::kR32G32B32A32_FLOAT: + return DXGI_FORMAT_R32G32B32A32_FLOAT; + case Format::kD32_FLOAT: + return DXGI_FORMAT_D32_FLOAT; + case Format::kBC1_UNORM: + return DXGI_FORMAT_BC1_UNORM; + case Format::kBC2_UNORM: + return DXGI_FORMAT_BC2_UNORM; + case Format::kBC3_UNORM: + return DXGI_FORMAT_BC3_UNORM; + case Format::kBC4_UNORM: + return DXGI_FORMAT_BC4_UNORM; + case Format::kBC5_UNORM: + return DXGI_FORMAT_BC5_UNORM; + default: + return DXGI_FORMAT_UNKNOWN; + } +} + +Format FromDxgi(DXGI_FORMAT format) { + switch (format) { + case DXGI_FORMAT_R8G8B8A8_UNORM: + return Format::kR8G8B8A8_UNORM; + case DXGI_FORMAT_R10G10B10A2_UNORM: + return Format::kR10G10B10A2_UNORM; + default: + return Format::kUnknown; + } +} + +D3D12_RESOURCE_STATES ToStates(ResourceState state, + D3D12_RESOURCE_STATES guest_output_state) { + switch (state) { + case ResourceState::kRenderTarget: + return D3D12_RESOURCE_STATE_RENDER_TARGET; + case ResourceState::kDepthWrite: + return D3D12_RESOURCE_STATE_DEPTH_WRITE; + case ResourceState::kPixelShaderResource: + return D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; + case ResourceState::kCopySource: + return D3D12_RESOURCE_STATE_COPY_SOURCE; + case ResourceState::kCopyDest: + return D3D12_RESOURCE_STATE_COPY_DEST; + case ResourceState::kGenericRead: + return D3D12_RESOURCE_STATE_GENERIC_READ; + case ResourceState::kGuestOutput: + return guest_output_state; + case ResourceState::kCommon: + default: + return D3D12_RESOURCE_STATE_COMMON; + } +} + +UINT8 ToShaderComponentMapping(const nrhi::Swizzle swizzle) { + switch (swizzle) { + case nrhi::Swizzle::kX: + return D3D12_SHADER_COMPONENT_MAPPING_FROM_MEMORY_COMPONENT_0; + case nrhi::Swizzle::kY: + return D3D12_SHADER_COMPONENT_MAPPING_FROM_MEMORY_COMPONENT_1; + case nrhi::Swizzle::kZ: + return D3D12_SHADER_COMPONENT_MAPPING_FROM_MEMORY_COMPONENT_2; + case nrhi::Swizzle::kW: + return D3D12_SHADER_COMPONENT_MAPPING_FROM_MEMORY_COMPONENT_3; + case nrhi::Swizzle::kZero: + return D3D12_SHADER_COMPONENT_MAPPING_FORCE_VALUE_0; + case nrhi::Swizzle::kOne: + default: + return D3D12_SHADER_COMPONENT_MAPPING_FORCE_VALUE_1; + } +} + +D3D12_BLEND ToBlend(nrhi::BlendFactor factor) { + switch (factor) { + case nrhi::BlendFactor::kZero: + return D3D12_BLEND_ZERO; + case nrhi::BlendFactor::kSrcAlpha: + return D3D12_BLEND_SRC_ALPHA; + case nrhi::BlendFactor::kInvSrcAlpha: + return D3D12_BLEND_INV_SRC_ALPHA; + case nrhi::BlendFactor::kOne: + default: + return D3D12_BLEND_ONE; + } +} + +D3D12_BLEND_OP ToBlendOp(nrhi::BlendOp op) { + return op == nrhi::BlendOp::kMin ? D3D12_BLEND_OP_MIN : D3D12_BLEND_OP_ADD; +} + +D3D12_COMPARISON_FUNC ToCompare(nrhi::CompareFunc func) { + switch (func) { + case nrhi::CompareFunc::kLess: + return D3D12_COMPARISON_FUNC_LESS; + case nrhi::CompareFunc::kLessEqual: + return D3D12_COMPARISON_FUNC_LESS_EQUAL; + case nrhi::CompareFunc::kAlways: + default: + return D3D12_COMPARISON_FUNC_ALWAYS; + } +} + +class NrDeviceD3D12; + +class NrBufferD3D12 : public nrhi::Buffer { + public: + uint64_t size() const override { return size_; } + + ID3D12Resource* resource = nullptr; + D3D12_GPU_VIRTUAL_ADDRESS gpu_va = 0; + void* mapping = nullptr; + uint64_t size_ = 0; + HeapKind heap = HeapKind::kDefault; +}; + +class NrTextureD3D12 : public nrhi::Texture { + public: + uint32_t width() const override { return desc.width; } + uint32_t height() const override { return desc.height; } + Format format() const override { return desc.format; } + + ID3D12Resource* resource = nullptr; + nrhi::TextureDesc desc; + DXGI_FORMAT dxgi_format = DXGI_FORMAT_UNKNOWN; + // RTV/DSV slots in the device's non-shader-visible heaps, ~0u when absent. + uint32_t rtv_slot = ~0u; + uint32_t dsv_slot = ~0u; + // Guest-output wrappers don't own their resource's lifetime semantics the + // same way (they hold a ref, but are never destroyed via DestroyDeferred). + bool is_guest_output = false; +}; + +class NrTextureViewD3D12 : public nrhi::TextureView { + public: + // Slot in the device's non-shader-visible staging CBV_SRV_UAV heap; the + // SRV descriptor is copied from here into shader-visible binding slots. + uint32_t staging_slot = ~0u; + NrTextureD3D12* texture = nullptr; +}; + +class NrBindingLayoutD3D12 : public nrhi::BindingLayout { + public: + ID3D12RootSignature* root_signature = nullptr; +}; + +class NrShaderD3D12 : public nrhi::Shader { + public: + ID3DBlob* blob = nullptr; +}; + +class NrPipelineD3D12 : public nrhi::Pipeline { + public: + ID3D12PipelineState* pso = nullptr; +}; + +// A simple slot allocator over a fixed-size descriptor heap with +// submission-keyed retirement (mirrors the scene renderer's original +// srv_next/srv_free/retired_srv_slots machinery). +struct SlotAllocator { + uint32_t capacity = 0; + uint32_t next = 0; + std::vector free_list; + // (first_slot, count, submission) + std::vector, uint64_t>> retired; + + bool Alloc(uint32_t count, uint32_t* out) { + if (count == 1 && !free_list.empty()) { + *out = free_list.back(); + free_list.pop_back(); + return true; + } + if (next + count <= capacity) { + *out = next; + next += count; + return true; + } + return false; + } + void Retire(uint32_t first, uint32_t count, uint64_t submission) { + retired.emplace_back(std::make_pair(first, count), submission); + } + void Drain(uint64_t completed) { + std::erase_if(retired, [&](const auto& entry) { + if (entry.second < completed) { + for (uint32_t i = 0; i < entry.first.second; ++i) { + free_list.push_back(entry.first.first + i); + } + return true; + } + return false; + }); + } +}; + +class NrCmdD3D12 : public nrhi::Cmd { + public: + void SetBindingLayout(nrhi::BindingLayout* layout) override; + void SetPipeline(nrhi::Pipeline* pipeline) override; + void SetRootConstants(uint32_t param, uint32_t count, const void* values, + uint32_t dest_offset_in_values) override; + void SetConstantBuffer(uint32_t param, nrhi::Buffer* buffer, + uint64_t offset) override; + void SetBufferSrv(uint32_t param, nrhi::Buffer* buffer, + uint64_t offset) override; + void SetTexture(uint32_t param, nrhi::TextureView* view) override; + void SetTexturePair(uint32_t param, nrhi::TextureView* first, + nrhi::TextureView* second) override; + void SetTextures(uint32_t param, nrhi::TextureView* const* views, + uint32_t count) override; + void SetRenderTargets(nrhi::Texture* color, nrhi::Texture* depth) override; + void ClearRenderTarget(nrhi::Texture* color, const float color4[4]) override; + void ClearDepth(nrhi::Texture* depth, float value) override; + void SetViewport(const nrhi::Viewport& viewport) override; + void SetScissor(const nrhi::Rect& rect) override; + void SetVertexBuffer(nrhi::Buffer* buffer, uint64_t offset, + uint32_t size_bytes, uint32_t stride) override; + void SetIndexBuffer(nrhi::Buffer* buffer, uint64_t offset, + uint32_t size_bytes) override; + void SetPrimitiveTopology(nrhi::PrimitiveTopology topology) override; + void Draw(uint32_t vertex_count, uint32_t start_vertex) override; + void DrawIndexed(uint32_t index_count, uint32_t start_index, + int32_t base_vertex) override; + void CopyBufferToTexture(nrhi::Texture* dst, uint32_t mip, + uint32_t array_slice, nrhi::Buffer* src, + uint64_t src_offset, uint32_t row_pitch, + uint32_t width, uint32_t height, + uint32_t depth) override; + void CopyTextureToBuffer(nrhi::Buffer* dst, uint64_t dst_offset, + uint32_t row_pitch, nrhi::Texture* src, + uint32_t mip, uint32_t width, + uint32_t height) override; + void Barrier(nrhi::Texture* texture, ResourceState before, + ResourceState after) override; + void FlushBarriers() override; + + NrDeviceD3D12* device = nullptr; +}; + +class NrDeviceD3D12 : public nrhi::Device { + public: + explicit NrDeviceD3D12(D3D12CommandProcessor* cp) : cp_(cp) { + const ui::d3d12::D3D12Provider& provider = cp_->GetD3D12Provider(); + device_ = provider.GetDevice(); + cmd_.device = this; + + D3D12_DESCRIPTOR_HEAP_DESC heap_desc{}; + heap_desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; + heap_desc.NumDescriptors = kStagingViews; + device_->CreateDescriptorHeap(&heap_desc, IID_PPV_ARGS(&staging_heap_)); + heap_desc.NumDescriptors = kShaderVisibleViews; + heap_desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; + device_->CreateDescriptorHeap(&heap_desc, IID_PPV_ARGS(&srv_heap_)); + heap_desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; + heap_desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; + heap_desc.NumDescriptors = kRtvSlots; + device_->CreateDescriptorHeap(&heap_desc, IID_PPV_ARGS(&rtv_heap_)); + heap_desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV; + heap_desc.NumDescriptors = kDsvSlots; + device_->CreateDescriptorHeap(&heap_desc, IID_PPV_ARGS(&dsv_heap_)); + + view_size_ = + device_->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV); + rtv_size_ = device_->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV); + dsv_size_ = device_->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV); + staging_slots_.capacity = kStagingViews; + srv_slots_.capacity = kShaderVisibleViews; + rtv_slots_.capacity = kRtvSlots; + dsv_slots_.capacity = kDsvSlots; + } + + ~NrDeviceD3D12() override { + // Callers guarantee GPU idle; release everything immediately. + DrainRetired(~0ull); + for (auto& entry : guest_outputs_) { + entry.second->resource->Release(); + delete entry.second; + } + if (staging_heap_) staging_heap_->Release(); + if (srv_heap_) srv_heap_->Release(); + if (rtv_heap_) rtv_heap_->Release(); + if (dsv_heap_) dsv_heap_->Release(); + } + + Backend backend() const override { return Backend::kD3D12; } + + nrhi::Buffer* CreateBuffer(const nrhi::BufferDesc& desc) override { + D3D12_RESOURCE_DESC rd{}; + rd.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; + rd.Width = desc.size; + rd.Height = 1; + rd.DepthOrArraySize = 1; + rd.MipLevels = 1; + rd.SampleDesc.Count = 1; + rd.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; + const D3D12_HEAP_PROPERTIES* heap = &ui::d3d12::util::kHeapPropertiesDefault; + D3D12_RESOURCE_STATES state = D3D12_RESOURCE_STATE_COMMON; + if (desc.heap == HeapKind::kUpload) { + heap = &ui::d3d12::util::kHeapPropertiesUpload; + state = D3D12_RESOURCE_STATE_GENERIC_READ; + } else if (desc.heap == HeapKind::kReadback) { + heap = &ui::d3d12::util::kHeapPropertiesReadback; + state = D3D12_RESOURCE_STATE_COPY_DEST; + } + ID3D12Resource* resource = nullptr; + if (FAILED(device_->CreateCommittedResource( + heap, cp_->GetD3D12Provider().GetHeapFlagCreateNotZeroed(), &rd, state, nullptr, + IID_PPV_ARGS(&resource)))) { + REXLOG_ERROR("nrhi-d3d12: buffer creation failed ({} bytes)", desc.size); + return nullptr; + } + auto* buffer = new NrBufferD3D12(); + buffer->resource = resource; + buffer->gpu_va = resource->GetGPUVirtualAddress(); + buffer->size_ = desc.size; + buffer->heap = desc.heap; + return buffer; + } + + nrhi::Texture* CreateTexture(const nrhi::TextureDesc& desc) override { + D3D12_RESOURCE_DESC rd{}; + rd.Dimension = desc.kind == TextureKind::k3D ? D3D12_RESOURCE_DIMENSION_TEXTURE3D + : D3D12_RESOURCE_DIMENSION_TEXTURE2D; + rd.Width = desc.width; + rd.Height = desc.height; + rd.DepthOrArraySize = + UINT16(desc.kind == TextureKind::kCube ? 6 : desc.kind == TextureKind::k3D ? desc.depth : 1); + rd.MipLevels = UINT16(desc.mip_levels); + rd.Format = ToDxgi(desc.format); + rd.SampleDesc.Count = desc.sample_count; + rd.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; + D3D12_CLEAR_VALUE clear{}; + const D3D12_CLEAR_VALUE* clear_ptr = nullptr; + if (desc.usage & nrhi::kTextureUsageRenderTarget) { + rd.Flags |= D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET; + clear.Format = rd.Format; + std::memcpy(clear.Color, desc.clear_color, sizeof(clear.Color)); + clear_ptr = &clear; + } + if (desc.usage & nrhi::kTextureUsageDepthStencil) { + // The depth resource is created typeless and viewed as D32_FLOAT / + // R32_FLOAT; a typed depth SRV read the clear value on some drivers + // (the photo-editor DoF smear); preserve the proven arrangement. + rd.Flags |= D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL; + if (desc.format == Format::kD32_FLOAT) { + rd.Format = DXGI_FORMAT_R32_TYPELESS; + } + clear.Format = DXGI_FORMAT_D32_FLOAT; + clear.DepthStencil.Depth = desc.clear_depth; + clear_ptr = &clear; + } + ID3D12Resource* resource = nullptr; + if (FAILED(device_->CreateCommittedResource( + &ui::d3d12::util::kHeapPropertiesDefault, + cp_->GetD3D12Provider().GetHeapFlagCreateNotZeroed(), &rd, + ToStates(desc.initial_state, guest_output_state_), clear_ptr, + IID_PPV_ARGS(&resource)))) { + REXLOG_ERROR("nrhi-d3d12: texture creation failed ({}x{} fmt {})", desc.width, + desc.height, uint32_t(desc.format)); + return nullptr; + } + auto* texture = new NrTextureD3D12(); + texture->resource = resource; + texture->desc = desc; + texture->dxgi_format = ToDxgi(desc.format); + if (desc.usage & nrhi::kTextureUsageRenderTarget) { + std::lock_guard lock(mutex_); + if (rtv_slots_.Alloc(1, &texture->rtv_slot)) { + device_->CreateRenderTargetView(resource, nullptr, RtvHandle(texture->rtv_slot)); + } + } + if (desc.usage & nrhi::kTextureUsageDepthStencil) { + std::lock_guard lock(mutex_); + if (dsv_slots_.Alloc(1, &texture->dsv_slot)) { + D3D12_DEPTH_STENCIL_VIEW_DESC dsv{}; + dsv.Format = DXGI_FORMAT_D32_FLOAT; + dsv.ViewDimension = desc.sample_count > 1 ? D3D12_DSV_DIMENSION_TEXTURE2DMS + : D3D12_DSV_DIMENSION_TEXTURE2D; + device_->CreateDepthStencilView(resource, &dsv, DsvHandle(texture->dsv_slot)); + } + } + return texture; + } + + void* Map(nrhi::Buffer* buffer) override { + auto* b = static_cast(buffer); + if (b->mapping == nullptr) { + const D3D12_RANGE no_read{}; + // Readback buffers are read through mapped memory; upload buffers are + // write-only (pass an empty read range). + b->resource->Map(0, b->heap == HeapKind::kReadback ? nullptr : &no_read, &b->mapping); + } + return b->mapping; + } + + void Unmap(nrhi::Buffer* buffer) override { + auto* b = static_cast(buffer); + if (b->mapping != nullptr) { + b->resource->Unmap(0, nullptr); + b->mapping = nullptr; + } + } + + void InvalidateForRead(nrhi::Buffer*, uint64_t, uint64_t) override {} + + void DestroyDeferred(nrhi::Buffer* buffer) override { + if (buffer == nullptr) return; + auto* b = static_cast(buffer); + std::lock_guard lock(mutex_); + retired_.emplace_back(RetiredObject{b->resource, nullptr, cp_->GetCurrentSubmission()}); + delete b; + } + + void DestroyDeferred(nrhi::Texture* texture) override { + if (texture == nullptr) return; + auto* t = static_cast(texture); + std::lock_guard lock(mutex_); + const uint64_t submission = cp_->GetCurrentSubmission(); + if (t->rtv_slot != ~0u) rtv_slots_.Retire(t->rtv_slot, 1, submission); + if (t->dsv_slot != ~0u) dsv_slots_.Retire(t->dsv_slot, 1, submission); + retired_.emplace_back(RetiredObject{t->resource, nullptr, submission}); + delete t; + } + + void DestroyDeferred(nrhi::TextureView* view) override { + if (view == nullptr) return; + auto* v = static_cast(view); + std::lock_guard lock(mutex_); + const uint64_t submission = cp_->GetCurrentSubmission(); + staging_slots_.Retire(v->staging_slot, 1, submission); + // Retire every shader-visible binding this view participates in. + std::erase_if(bindings_, [&](auto& entry) { + for (uint32_t i = 0; i < entry.first.count; ++i) { + if (entry.first.views[i] == v) { + srv_slots_.Retire(entry.second.first_slot, entry.second.count, submission); + return true; + } + } + return false; + }); + delete v; + } + + void DestroyDeferred(nrhi::Pipeline* pipeline) override { + if (pipeline == nullptr) return; + auto* p = static_cast(pipeline); + std::lock_guard lock(mutex_); + retired_.emplace_back(RetiredObject{nullptr, p->pso, cp_->GetCurrentSubmission()}); + delete p; + } + + void DestroyDeferred(nrhi::Shader* shader) override { + if (shader == nullptr) return; + auto* s = static_cast(shader); + if (s->blob) s->blob->Release(); + delete s; + } + + nrhi::TextureView* CreateTextureView(nrhi::Texture* texture, + const nrhi::TextureViewDesc& desc) override { + auto* t = static_cast(texture); + uint32_t slot; + { + std::lock_guard lock(mutex_); + if (!staging_slots_.Alloc(1, &slot)) { + REXLOG_ERROR("nrhi-d3d12: staging view heap exhausted"); + return nullptr; + } + } + D3D12_SHADER_RESOURCE_VIEW_DESC srv{}; + srv.Format = desc.format != Format::kUnknown ? ToDxgi(desc.format) + : t->desc.format == Format::kD32_FLOAT ? DXGI_FORMAT_R32_FLOAT + : t->dxgi_format; + srv.Shader4ComponentMapping = D3D12_ENCODE_SHADER_4_COMPONENT_MAPPING( + ToShaderComponentMapping(desc.swizzle[0]), ToShaderComponentMapping(desc.swizzle[1]), + ToShaderComponentMapping(desc.swizzle[2]), ToShaderComponentMapping(desc.swizzle[3])); + const uint32_t mips = desc.mip_levels == ~0u + ? (t->desc.mip_levels - desc.base_mip) + : desc.mip_levels; + switch (desc.dimension) { + case nrhi::ViewDimension::k2DMS: + srv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2DMS; + break; + case nrhi::ViewDimension::kCube: + srv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURECUBE; + srv.TextureCube.MostDetailedMip = desc.base_mip; + srv.TextureCube.MipLevels = mips; + break; + case nrhi::ViewDimension::k3D: + srv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE3D; + srv.Texture3D.MostDetailedMip = desc.base_mip; + srv.Texture3D.MipLevels = mips; + break; + case nrhi::ViewDimension::k2D: + default: + srv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; + srv.Texture2D.MostDetailedMip = desc.base_mip; + srv.Texture2D.MipLevels = mips; + break; + } + device_->CreateShaderResourceView(t->resource, &srv, StagingHandle(slot)); + auto* view = new NrTextureViewD3D12(); + view->staging_slot = slot; + view->texture = t; + return view; + } + + nrhi::BindingLayout* CreateBindingLayout(const nrhi::BindingLayoutDesc& desc) override { + D3D12_ROOT_PARAMETER params[nrhi::kMaxBindingParams] = {}; + D3D12_DESCRIPTOR_RANGE ranges[nrhi::kMaxBindingParams] = {}; + for (uint32_t i = 0; i < desc.param_count; ++i) { + const nrhi::BindingParamDesc& p = desc.params[i]; + D3D12_SHADER_VISIBILITY visibility = + p.visibility == nrhi::Visibility::kVertex ? D3D12_SHADER_VISIBILITY_VERTEX + : p.visibility == nrhi::Visibility::kPixel ? D3D12_SHADER_VISIBILITY_PIXEL + : D3D12_SHADER_VISIBILITY_ALL; + params[i].ShaderVisibility = visibility; + switch (p.kind) { + case nrhi::BindingParamKind::kConstants: + params[i].ParameterType = D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS; + params[i].Constants.ShaderRegister = p.shader_register; + params[i].Constants.Num32BitValues = p.count; + break; + case nrhi::BindingParamKind::kConstantBuffer: + params[i].ParameterType = D3D12_ROOT_PARAMETER_TYPE_CBV; + params[i].Descriptor.ShaderRegister = p.shader_register; + break; + case nrhi::BindingParamKind::kBufferSrv: + params[i].ParameterType = D3D12_ROOT_PARAMETER_TYPE_SRV; + params[i].Descriptor.ShaderRegister = p.shader_register; + break; + case nrhi::BindingParamKind::kTextureTable: + ranges[i].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV; + ranges[i].NumDescriptors = p.count; + ranges[i].BaseShaderRegister = p.shader_register; + params[i].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; + params[i].DescriptorTable.NumDescriptorRanges = 1; + params[i].DescriptorTable.pDescriptorRanges = &ranges[i]; + break; + } + } + D3D12_STATIC_SAMPLER_DESC samplers[nrhi::kMaxStaticSamplers] = {}; + for (uint32_t i = 0; i < desc.static_sampler_count; ++i) { + const nrhi::StaticSamplerDesc& s = desc.static_samplers[i]; + samplers[i].Filter = s.filter == nrhi::Filter::kAnisotropic ? D3D12_FILTER_ANISOTROPIC + : s.filter == nrhi::Filter::kLinear + ? D3D12_FILTER_MIN_MAG_MIP_LINEAR + : D3D12_FILTER_MIN_MAG_MIP_POINT; + samplers[i].MaxAnisotropy = s.max_anisotropy; + D3D12_TEXTURE_ADDRESS_MODE mode = s.address == nrhi::AddressMode::kWrap + ? D3D12_TEXTURE_ADDRESS_MODE_WRAP + : D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + samplers[i].AddressU = mode; + samplers[i].AddressV = mode; + samplers[i].AddressW = mode; + samplers[i].MaxLOD = D3D12_FLOAT32_MAX; + samplers[i].ShaderRegister = s.shader_register; + samplers[i].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; + } + D3D12_ROOT_SIGNATURE_DESC rs{}; + rs.NumParameters = desc.param_count; + rs.pParameters = params; + rs.NumStaticSamplers = desc.static_sampler_count; + rs.pStaticSamplers = samplers; + rs.Flags = desc.allow_input_layout ? D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT + : D3D12_ROOT_SIGNATURE_FLAG_NONE; + ID3D12RootSignature* root_signature = + ui::d3d12::util::CreateRootSignature(cp_->GetD3D12Provider(), rs); + if (root_signature == nullptr) { + REXLOG_ERROR("nrhi-d3d12: root signature creation failed"); + return nullptr; + } + auto* layout = new NrBindingLayoutD3D12(); + layout->root_signature = root_signature; + return layout; + } + + nrhi::Shader* CreateShader(const nrhi::ShaderDesc& desc) override { + D3D_SHADER_MACRO macros[9] = {}; + uint32_t macro_count = 0; + if (desc.macros != nullptr) { + while (desc.macros[macro_count].name != nullptr && macro_count < 8) { + macros[macro_count].Name = desc.macros[macro_count].name; + macros[macro_count].Definition = desc.macros[macro_count].value; + ++macro_count; + } + } + ID3DBlob* blob = nullptr; + ID3DBlob* errors = nullptr; + const char* target = desc.stage == nrhi::ShaderStage::kVertex ? "vs_5_0" : "ps_5_0"; + HRESULT hr = D3DCompile(desc.hlsl_source, std::strlen(desc.hlsl_source), + desc.name != nullptr ? desc.name : "nrhi_shader", + macro_count != 0 ? macros : nullptr, nullptr, desc.entry_point, + target, 0, 0, &blob, &errors); + if (FAILED(hr)) { + REXLOG_ERROR("nrhi-d3d12: shader compile failed ({} {}): {}", + desc.name != nullptr ? desc.name : "?", desc.entry_point, + errors != nullptr ? static_cast(errors->GetBufferPointer()) + : "no error blob"); + if (errors) errors->Release(); + return nullptr; + } + if (errors) errors->Release(); + auto* shader = new NrShaderD3D12(); + shader->blob = blob; + return shader; + } + + nrhi::Pipeline* CreateGraphicsPipeline(const nrhi::GraphicsPipelineDesc& desc) override { + auto* layout = static_cast(desc.layout); + auto* vs = static_cast(desc.vs); + auto* ps = static_cast(desc.ps); + if (layout == nullptr || vs == nullptr || ps == nullptr) return nullptr; + D3D12_GRAPHICS_PIPELINE_STATE_DESC pd{}; + pd.pRootSignature = layout->root_signature; + pd.VS = {vs->blob->GetBufferPointer(), vs->blob->GetBufferSize()}; + pd.PS = {ps->blob->GetBufferPointer(), ps->blob->GetBufferSize()}; + pd.BlendState.RenderTarget[0].BlendEnable = desc.blend.enable; + pd.BlendState.RenderTarget[0].SrcBlend = ToBlend(desc.blend.src); + pd.BlendState.RenderTarget[0].DestBlend = ToBlend(desc.blend.dst); + pd.BlendState.RenderTarget[0].BlendOp = ToBlendOp(desc.blend.op); + pd.BlendState.RenderTarget[0].SrcBlendAlpha = ToBlend(desc.blend.src_alpha); + pd.BlendState.RenderTarget[0].DestBlendAlpha = ToBlend(desc.blend.dst_alpha); + pd.BlendState.RenderTarget[0].BlendOpAlpha = ToBlendOp(desc.blend.op_alpha); + pd.BlendState.RenderTarget[0].RenderTargetWriteMask = desc.blend.write_mask; + pd.SampleMask = UINT_MAX; + pd.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID; + pd.RasterizerState.CullMode = desc.cull == nrhi::CullMode::kFront ? D3D12_CULL_MODE_FRONT + : desc.cull == nrhi::CullMode::kBack ? D3D12_CULL_MODE_BACK + : D3D12_CULL_MODE_NONE; + pd.RasterizerState.DepthClipEnable = desc.depth_clip; + pd.DepthStencilState.DepthEnable = desc.depth.test_enable; + pd.DepthStencilState.DepthWriteMask = desc.depth.write_enable + ? D3D12_DEPTH_WRITE_MASK_ALL + : D3D12_DEPTH_WRITE_MASK_ZERO; + pd.DepthStencilState.DepthFunc = ToCompare(desc.depth.func); + D3D12_INPUT_ELEMENT_DESC elements[16] = {}; + if (desc.input_elements != nullptr && desc.input_element_count != 0 && + desc.input_element_count <= 16) { + for (uint32_t i = 0; i < desc.input_element_count; ++i) { + const nrhi::InputElementDesc& e = desc.input_elements[i]; + elements[i].SemanticName = e.semantic_name; + elements[i].SemanticIndex = e.semantic_index; + elements[i].Format = ToDxgi(e.format); + elements[i].AlignedByteOffset = e.byte_offset; + elements[i].InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA; + } + pd.InputLayout.pInputElementDescs = elements; + pd.InputLayout.NumElements = desc.input_element_count; + } + pd.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; + if (desc.rtv_format != Format::kUnknown) { + pd.NumRenderTargets = 1; + pd.RTVFormats[0] = ToDxgi(desc.rtv_format); + } + if (desc.dsv_format != Format::kUnknown) { + pd.DSVFormat = ToDxgi(desc.dsv_format); + } + pd.SampleDesc.Count = desc.sample_count; + ID3D12PipelineState* pso = nullptr; + if (FAILED(device_->CreateGraphicsPipelineState(&pd, IID_PPV_ARGS(&pso)))) { + REXLOG_ERROR("nrhi-d3d12: graphics pipeline creation failed"); + return nullptr; + } + auto* pipeline = new NrPipelineD3D12(); + pipeline->pso = pso; + return pipeline; + } + + uint32_t GetSupportedSampleCount(Format format, uint32_t desired) override { + DXGI_FORMAT dxgi = ToDxgi(format); + uint32_t count = desired; + while (count > 1) { + D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS levels{}; + levels.Format = dxgi; + levels.SampleCount = count; + if (SUCCEEDED(device_->CheckFeatureSupport(D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS, + &levels, sizeof(levels))) && + levels.NumQualityLevels > 0) { + break; + } + count >>= 1; + } + return std::max(count, 1u); + } + + uint64_t CurrentSubmission() const override { return cp_->GetCurrentSubmission(); } + uint64_t CompletedSubmission() const override { return cp_->GetCompletedSubmission(); } + + // --- frame handling (called from the command processor) --- + + nrhi::Cmd* BeginFrame(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) { + guest_output_state_ = guest_output_internal_state; + { + std::lock_guard lock(mutex_); + DrainRetired(cp_->GetCompletedSubmission()); + } + NrTextureD3D12*& wrapper = guest_outputs_[guest_output_resource]; + if (wrapper != nullptr && + (wrapper->desc.width != width || wrapper->desc.height != height)) { + // Same resource pointer, different geometry: a recreated image reusing + // the address. Retire the stale wrapper's views and slots. + DestroyGuestOutputWrapper(wrapper); + wrapper = nullptr; + } + if (wrapper == nullptr) { + wrapper = new NrTextureD3D12(); + guest_output_resource->AddRef(); + wrapper->resource = guest_output_resource; + wrapper->desc.width = width; + wrapper->desc.height = height; + wrapper->desc.format = FromDxgi(guest_output_format); + wrapper->desc.usage = nrhi::kTextureUsageRenderTarget; + wrapper->dxgi_format = guest_output_format; + wrapper->is_guest_output = true; + std::lock_guard lock(mutex_); + if (rtv_slots_.Alloc(1, &wrapper->rtv_slot)) { + device_->CreateRenderTargetView(guest_output_resource, nullptr, + RtvHandle(wrapper->rtv_slot)); + } + // The presenter keeps a small guest-output mailbox; evict wrappers of + // resources it has replaced. + if (guest_outputs_.size() > 6) { + for (auto it = guest_outputs_.begin(); it != guest_outputs_.end();) { + if (it->second != wrapper && it->first != guest_output_resource) { + DestroyGuestOutputWrapperLocked(it->second); + it = guest_outputs_.erase(it); + } else { + ++it; + } + } + } + } + *guest_output_out = wrapper; + return &cmd_; + } + + // --- internals shared with NrCmdD3D12 --- + + D3D12_CPU_DESCRIPTOR_HANDLE StagingHandle(uint32_t slot) const { + D3D12_CPU_DESCRIPTOR_HANDLE handle = + staging_heap_->GetCPUDescriptorHandleForHeapStart(); + handle.ptr += size_t(slot) * view_size_; + return handle; + } + D3D12_CPU_DESCRIPTOR_HANDLE SrvCpuHandle(uint32_t slot) const { + D3D12_CPU_DESCRIPTOR_HANDLE handle = srv_heap_->GetCPUDescriptorHandleForHeapStart(); + handle.ptr += size_t(slot) * view_size_; + return handle; + } + D3D12_GPU_DESCRIPTOR_HANDLE SrvGpuHandle(uint32_t slot) const { + D3D12_GPU_DESCRIPTOR_HANDLE handle = srv_heap_->GetGPUDescriptorHandleForHeapStart(); + handle.ptr += size_t(slot) * view_size_; + return handle; + } + D3D12_CPU_DESCRIPTOR_HANDLE RtvHandle(uint32_t slot) const { + D3D12_CPU_DESCRIPTOR_HANDLE handle = rtv_heap_->GetCPUDescriptorHandleForHeapStart(); + handle.ptr += size_t(slot) * rtv_size_; + return handle; + } + D3D12_CPU_DESCRIPTOR_HANDLE DsvHandle(uint32_t slot) const { + D3D12_CPU_DESCRIPTOR_HANDLE handle = dsv_heap_->GetCPUDescriptorHandleForHeapStart(); + handle.ptr += size_t(slot) * dsv_size_; + return handle; + } + + // Shader-visible binding for an ordered view tuple: consecutive slots + // holding copies of the staging descriptors, cached and retired when a + // participating view is destroyed. Render thread only. + bool GetBinding(NrTextureViewD3D12* const* views, uint32_t count, + D3D12_GPU_DESCRIPTOR_HANDLE* gpu_out) { + if (count == 0 || count > nrhi::kMaxTextureTableSize || views[0] == nullptr) { + return false; + } + BindingKey key{}; + for (uint32_t i = 0; i < count; ++i) { + key.views[i] = views[i]; + } + key.count = count; + auto it = bindings_.find(key); + if (it == bindings_.end()) { + uint32_t first_slot; + { + std::lock_guard lock(mutex_); + if (!srv_slots_.Alloc(count, &first_slot)) { + REXLOG_ERROR("nrhi-d3d12: shader-visible view heap exhausted"); + return false; + } + } + for (uint32_t i = 0; i < count; ++i) { + if (views[i] == nullptr) continue; + device_->CopyDescriptorsSimple(1, SrvCpuHandle(first_slot + i), + StagingHandle(views[i]->staging_slot), + D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV); + } + it = bindings_.emplace(key, Binding{first_slot, count}).first; + } + *gpu_out = SrvGpuHandle(it->second.first_slot); + return true; + } + + D3D12CommandProcessor* cp() { return cp_; } + ID3D12DescriptorHeap* srv_heap() { return srv_heap_; } + D3D12_RESOURCE_STATES guest_output_state() const { return guest_output_state_; } + + private: + static constexpr uint32_t kStagingViews = 16384; + static constexpr uint32_t kShaderVisibleViews = 32768; + static constexpr uint32_t kRtvSlots = 64; + static constexpr uint32_t kDsvSlots = 8; + + struct RetiredObject { + ID3D12Resource* resource; + ID3D12PipelineState* pso; + uint64_t submission; + }; + struct BindingKey { + NrTextureViewD3D12* views[nrhi::kMaxTextureTableSize]; + uint32_t count; + bool operator<(const BindingKey& other) const { + if (count != other.count) return count < other.count; + for (uint32_t i = 0; i < count; ++i) { + if (views[i] != other.views[i]) return views[i] < other.views[i]; + } + return false; + } + }; + struct Binding { + uint32_t first_slot; + uint32_t count; + }; + + void DrainRetired(uint64_t completed) { + std::erase_if(retired_, [&](const RetiredObject& r) { + if (r.submission < completed) { + if (r.resource) r.resource->Release(); + if (r.pso) r.pso->Release(); + return true; + } + return false; + }); + staging_slots_.Drain(completed); + srv_slots_.Drain(completed); + rtv_slots_.Drain(completed); + dsv_slots_.Drain(completed); + } + + void DestroyGuestOutputWrapperLocked(NrTextureD3D12* wrapper) { + const uint64_t submission = cp_->GetCurrentSubmission(); + if (wrapper->rtv_slot != ~0u) rtv_slots_.Retire(wrapper->rtv_slot, 1, submission); + retired_.emplace_back(RetiredObject{wrapper->resource, nullptr, submission}); + delete wrapper; + } + void DestroyGuestOutputWrapper(NrTextureD3D12* wrapper) { + std::lock_guard lock(mutex_); + DestroyGuestOutputWrapperLocked(wrapper); + } + + D3D12CommandProcessor* cp_; + ID3D12Device* device_ = nullptr; + NrCmdD3D12 cmd_; + + ID3D12DescriptorHeap* staging_heap_ = nullptr; + ID3D12DescriptorHeap* srv_heap_ = nullptr; + ID3D12DescriptorHeap* rtv_heap_ = nullptr; + ID3D12DescriptorHeap* dsv_heap_ = nullptr; + uint32_t view_size_ = 0; + uint32_t rtv_size_ = 0; + uint32_t dsv_size_ = 0; + + // mutex_ guards the slot allocators and retired_ (creation and deferred + // destruction are thread-safe); bindings_ is render-thread-only. + std::mutex mutex_; + SlotAllocator staging_slots_; + SlotAllocator srv_slots_; + SlotAllocator rtv_slots_; + SlotAllocator dsv_slots_; + std::vector retired_; + std::map bindings_; + std::map guest_outputs_; + D3D12_RESOURCE_STATES guest_output_state_ = D3D12_RESOURCE_STATE_COMMON; +}; + +void NrCmdD3D12::SetBindingLayout(nrhi::BindingLayout* layout) { + auto* l = static_cast(layout); + DeferredCommandList& list = device->cp()->GetDeferredCommandList(); + list.SetDescriptorHeaps(device->srv_heap(), nullptr); + list.D3DSetGraphicsRootSignature(l->root_signature); +} + +void NrCmdD3D12::SetPipeline(nrhi::Pipeline* pipeline) { + device->cp()->GetDeferredCommandList().D3DSetPipelineState( + static_cast(pipeline)->pso); +} + +void NrCmdD3D12::SetRootConstants(uint32_t param, uint32_t count, const void* values, + uint32_t dest_offset_in_values) { + device->cp()->GetDeferredCommandList().D3DSetGraphicsRoot32BitConstants( + param, count, values, dest_offset_in_values); +} + +void NrCmdD3D12::SetConstantBuffer(uint32_t param, nrhi::Buffer* buffer, uint64_t offset) { + device->cp()->GetDeferredCommandList().D3DSetGraphicsRootConstantBufferView( + param, static_cast(buffer)->gpu_va + offset); +} + +void NrCmdD3D12::SetBufferSrv(uint32_t param, nrhi::Buffer* buffer, uint64_t offset) { + device->cp()->GetDeferredCommandList().D3DSetGraphicsRootShaderResourceView( + param, static_cast(buffer)->gpu_va + offset); +} + +void NrCmdD3D12::SetTexture(uint32_t param, nrhi::TextureView* view) { + NrTextureViewD3D12* views[1] = {static_cast(view)}; + D3D12_GPU_DESCRIPTOR_HANDLE handle; + if (device->GetBinding(views, 1, &handle)) { + device->cp()->GetDeferredCommandList().D3DSetGraphicsRootDescriptorTable(param, handle); + } +} + +void NrCmdD3D12::SetTexturePair(uint32_t param, nrhi::TextureView* first, + nrhi::TextureView* second) { + NrTextureViewD3D12* views[2] = {static_cast(first), + static_cast(second)}; + D3D12_GPU_DESCRIPTOR_HANDLE handle; + if (device->GetBinding(views, 2, &handle)) { + device->cp()->GetDeferredCommandList().D3DSetGraphicsRootDescriptorTable(param, handle); + } +} + +void NrCmdD3D12::SetTextures(uint32_t param, nrhi::TextureView* const* views, uint32_t count) { + NrTextureViewD3D12* typed[nrhi::kMaxTextureTableSize] = {}; + if (count > nrhi::kMaxTextureTableSize) return; + for (uint32_t i = 0; i < count; ++i) { + typed[i] = static_cast(views[i]); + } + D3D12_GPU_DESCRIPTOR_HANDLE handle; + if (device->GetBinding(typed, count, &handle)) { + device->cp()->GetDeferredCommandList().D3DSetGraphicsRootDescriptorTable(param, handle); + } +} + +void NrCmdD3D12::SetRenderTargets(nrhi::Texture* color, nrhi::Texture* depth) { + D3D12_CPU_DESCRIPTOR_HANDLE rtv; + D3D12_CPU_DESCRIPTOR_HANDLE dsv; + const D3D12_CPU_DESCRIPTOR_HANDLE* rtv_ptr = nullptr; + const D3D12_CPU_DESCRIPTOR_HANDLE* dsv_ptr = nullptr; + UINT num_rtvs = 0; + if (color != nullptr) { + rtv = device->RtvHandle(static_cast(color)->rtv_slot); + rtv_ptr = &rtv; + num_rtvs = 1; + } + if (depth != nullptr) { + dsv = device->DsvHandle(static_cast(depth)->dsv_slot); + dsv_ptr = &dsv; + } + device->cp()->GetDeferredCommandList().D3DOMSetRenderTargets(num_rtvs, rtv_ptr, FALSE, + dsv_ptr); +} + +void NrCmdD3D12::ClearRenderTarget(nrhi::Texture* color, const float color4[4]) { + device->cp()->GetDeferredCommandList().D3DClearRenderTargetView( + device->RtvHandle(static_cast(color)->rtv_slot), color4, 0, nullptr); +} + +void NrCmdD3D12::ClearDepth(nrhi::Texture* depth, float value) { + device->cp()->GetDeferredCommandList().D3DClearDepthStencilView( + device->DsvHandle(static_cast(depth)->dsv_slot), + D3D12_CLEAR_FLAG_DEPTH, value, 0, 0, nullptr); +} + +void NrCmdD3D12::SetViewport(const nrhi::Viewport& viewport) { + D3D12_VIEWPORT vp; + vp.TopLeftX = viewport.x; + vp.TopLeftY = viewport.y; + vp.Width = viewport.width; + vp.Height = viewport.height; + vp.MinDepth = viewport.min_depth; + vp.MaxDepth = viewport.max_depth; + device->cp()->GetDeferredCommandList().RSSetViewport(vp); +} + +void NrCmdD3D12::SetScissor(const nrhi::Rect& rect) { + D3D12_RECT r; + r.left = rect.left; + r.top = rect.top; + r.right = rect.right; + r.bottom = rect.bottom; + device->cp()->GetDeferredCommandList().RSSetScissorRect(r); +} + +void NrCmdD3D12::SetVertexBuffer(nrhi::Buffer* buffer, uint64_t offset, uint32_t size_bytes, + uint32_t stride) { + D3D12_VERTEX_BUFFER_VIEW view; + view.BufferLocation = static_cast(buffer)->gpu_va + offset; + view.SizeInBytes = size_bytes; + view.StrideInBytes = stride; + device->cp()->GetDeferredCommandList().D3DIASetVertexBuffers(0, 1, &view); +} + +void NrCmdD3D12::SetIndexBuffer(nrhi::Buffer* buffer, uint64_t offset, uint32_t size_bytes) { + D3D12_INDEX_BUFFER_VIEW view; + view.BufferLocation = static_cast(buffer)->gpu_va + offset; + view.SizeInBytes = size_bytes; + view.Format = DXGI_FORMAT_R16_UINT; + device->cp()->GetDeferredCommandList().D3DIASetIndexBuffer(&view); +} + +void NrCmdD3D12::SetPrimitiveTopology(nrhi::PrimitiveTopology topology) { + device->cp()->GetDeferredCommandList().D3DIASetPrimitiveTopology( + topology == nrhi::PrimitiveTopology::kTriangleStrip ? D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP + : D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); +} + +void NrCmdD3D12::Draw(uint32_t vertex_count, uint32_t start_vertex) { + device->cp()->GetDeferredCommandList().D3DDrawInstanced(vertex_count, 1, start_vertex, 0); +} + +void NrCmdD3D12::DrawIndexed(uint32_t index_count, uint32_t start_index, int32_t base_vertex) { + device->cp()->GetDeferredCommandList().D3DDrawIndexedInstanced(index_count, 1, start_index, + base_vertex, 0); +} + +void NrCmdD3D12::CopyBufferToTexture(nrhi::Texture* dst, uint32_t mip, uint32_t array_slice, + nrhi::Buffer* src, uint64_t src_offset, uint32_t row_pitch, + uint32_t width, uint32_t height, uint32_t depth) { + auto* t = static_cast(dst); + D3D12_TEXTURE_COPY_LOCATION dst_loc{}; + dst_loc.pResource = t->resource; + dst_loc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + dst_loc.SubresourceIndex = mip + array_slice * t->desc.mip_levels; + D3D12_TEXTURE_COPY_LOCATION src_loc{}; + src_loc.pResource = static_cast(src)->resource; + src_loc.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; + src_loc.PlacedFootprint.Offset = src_offset; + src_loc.PlacedFootprint.Footprint.Format = t->dxgi_format; + src_loc.PlacedFootprint.Footprint.Width = width; + src_loc.PlacedFootprint.Footprint.Height = height; + src_loc.PlacedFootprint.Footprint.Depth = depth; + src_loc.PlacedFootprint.Footprint.RowPitch = row_pitch; + device->cp()->GetDeferredCommandList().D3DCopyTextureRegion(&dst_loc, 0, 0, 0, &src_loc, + nullptr); +} + +void NrCmdD3D12::CopyTextureToBuffer(nrhi::Buffer* dst, uint64_t dst_offset, uint32_t row_pitch, + nrhi::Texture* src, uint32_t mip, uint32_t width, + uint32_t height) { + auto* t = static_cast(src); + D3D12_TEXTURE_COPY_LOCATION src_loc{}; + src_loc.pResource = t->resource; + src_loc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + src_loc.SubresourceIndex = mip; + D3D12_TEXTURE_COPY_LOCATION dst_loc{}; + dst_loc.pResource = static_cast(dst)->resource; + dst_loc.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; + dst_loc.PlacedFootprint.Offset = dst_offset; + dst_loc.PlacedFootprint.Footprint.Format = t->dxgi_format; + dst_loc.PlacedFootprint.Footprint.Width = width; + dst_loc.PlacedFootprint.Footprint.Height = height; + dst_loc.PlacedFootprint.Footprint.Depth = 1; + dst_loc.PlacedFootprint.Footprint.RowPitch = row_pitch; + device->cp()->GetDeferredCommandList().D3DCopyTextureRegion(&dst_loc, 0, 0, 0, &src_loc, + nullptr); +} + +void NrCmdD3D12::Barrier(nrhi::Texture* texture, ResourceState before, ResourceState after) { + auto* t = static_cast(texture); + device->cp()->PushTransitionBarrier(t->resource, + ToStates(before, device->guest_output_state()), + ToStates(after, device->guest_output_state())); +} + +void NrCmdD3D12::FlushBarriers() { device->cp()->SubmitBarriers(); } + +} // namespace + +nrhi::Device* CreateNativeRhiDevice(D3D12CommandProcessor* command_processor) { + return new NrDeviceD3D12(command_processor); +} + +void DestroyNativeRhiDevice(nrhi::Device* device) { + delete static_cast(device); +} + +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) { + return static_cast(device)->BeginFrame( + guest_output_resource, guest_output_format, guest_output_internal_state, width, height, + guest_output_out); +} + +} // namespace rex::graphics::d3d12 diff --git a/src/graphics/native_guest_renderer.cpp b/src/graphics/native_guest_renderer.cpp index f1bc3b8..610ae4e 100644 --- a/src/graphics/native_guest_renderer.cpp +++ b/src/graphics/native_guest_renderer.cpp @@ -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 diff --git a/src/graphics/vulkan/command_processor.cpp b/src/graphics/vulkan/command_processor.cpp index 3c7ee4a..af4cf7a 100644 --- a/src/graphics/vulkan/command_processor.cpp +++ b/src/graphics/vulkan/command_processor.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -29,14 +30,17 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include +#include #include #include #include @@ -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(); + 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(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().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().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().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 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(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; diff --git a/src/graphics/vulkan/deferred_command_buffer.cpp b/src/graphics/vulkan/deferred_command_buffer.cpp index 1ad1712..44c1ac8 100644 --- a/src/graphics/vulkan/deferred_command_buffer.cpp +++ b/src/graphics/vulkan/deferred_command_buffer.cpp @@ -167,6 +167,16 @@ void DeferredCommandBuffer::Execute(VkCommandBuffer command_buffer) { rex::align(sizeof(ArgsVkCopyBufferToImage), alignof(VkBufferImageCopy)))); } break; + case Command::kVkCopyImageToBuffer: { + auto& args = *reinterpret_cast(stream); + dfn.vkCmdCopyImageToBuffer( + command_buffer, args.src_image, args.src_image_layout, args.dst_buffer, + args.region_count, + reinterpret_cast( + reinterpret_cast(stream) + + rex::align(sizeof(ArgsVkCopyImageToBuffer), alignof(VkBufferImageCopy)))); + } break; + case Command::kVkCopyQueryPoolResults: { auto& args = *reinterpret_cast(stream); dfn.vkCmdCopyQueryPoolResults(command_buffer, args.query_pool, args.first_query, diff --git a/src/graphics/vulkan/native_rhi_vulkan.cpp b/src/graphics/vulkan/native_rhi_vulkan.cpp new file mode 100644 index 0000000..8d92d27 --- /dev/null +++ b/src/graphics/vulkan/native_rhi_vulkan.cpp @@ -0,0 +1,2585 @@ +// Vulkan implementation of the native-render RHI. Built on the Vulkan +// command processor's deferred command buffer, barrier queue and submission +// counters. The D3D12 backend is a passthrough; this backend owns all the +// translation: +// - explicit resource states -> (stage mask, access mask, image layout), +// with the current layout tracked per texture (first-use transitions come +// from UNDEFINED and discard, exactly what D3D12 initial states allowed); +// - render targets -> classic render pass + framebuffer caches with lazy +// begin at the first draw and end on target changes / copies / barrier +// flushes (no dynamic-rendering dependency - one tested path); +// - the binding model -> the frozen Vulkan set/binding plan: +// set 0 = buffer params in +// param order as dynamic-offset UBO/SSBO descriptors + immutable +// samplers, sets 1..N = one per texture-table param; +// - 32-bit root constants -> a CPU shadow block copied per draw into a +// 256-aligned slice of an internal per-frame ring UBO bound with a +// dynamic offset; +// - D3D top-left raster space -> negative-height viewport (core 1.1 +// maintenance1 behavior, the backend targets 1.3) with front face +// CLOCKWISE, so the geometry->pixel mapping and screen-space winding +// match D3D12 with unchanged shaders. +// +// Threading: Create*/Map/Unmap/InvalidateForRead/DestroyDeferred are +// thread-safe (VMA in thread-safe mode + a mutex over the backend's own +// bookkeeping); views, binding layouts, pipelines, shaders and all Cmd +// methods are render-thread-only per the interface contract (no locks +// taken on the caches they touch). + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace rex::graphics::vulkan { +namespace { + +using nrhi::Backend; +using nrhi::Format; +using nrhi::HeapKind; +using nrhi::ResourceState; +using nrhi::TextureKind; + +// --------------------------------------------------------------------------- +// Format / state / enum translation. +// --------------------------------------------------------------------------- + +VkFormat ToVkFormat(Format format) { + switch (format) { + case Format::kR8G8B8A8_UNORM: + return VK_FORMAT_R8G8B8A8_UNORM; + case Format::kR8G8B8A8_UINT: + return VK_FORMAT_R8G8B8A8_UINT; + case Format::kR8_UNORM: + return VK_FORMAT_R8_UNORM; + case Format::kR8G8_UNORM: + return VK_FORMAT_R8G8_UNORM; + case Format::kB5G6R5_UNORM: + // Same bit layout as DXGI_FORMAT_B5G6R5_UNORM (G in the middle, R in + // the top bits of the packed 16). + return VK_FORMAT_R5G6B5_UNORM_PACK16; + case Format::kR16G16_UNORM: + return VK_FORMAT_R16G16_UNORM; + case Format::kR10G10B10A2_UNORM: + // Identical bit layout to DXGI_FORMAT_R10G10B10A2_UNORM. + return VK_FORMAT_A2B10G10R10_UNORM_PACK32; + case Format::kR32_FLOAT: + return VK_FORMAT_R32_SFLOAT; + case Format::kR32G32_FLOAT: + return VK_FORMAT_R32G32_SFLOAT; + case Format::kR32G32B32_FLOAT: + return VK_FORMAT_R32G32B32_SFLOAT; + case Format::kR32G32B32A32_FLOAT: + return VK_FORMAT_R32G32B32A32_SFLOAT; + case Format::kD32_FLOAT: + return VK_FORMAT_D32_SFLOAT; + case Format::kBC1_UNORM: + return VK_FORMAT_BC1_RGBA_UNORM_BLOCK; + case Format::kBC2_UNORM: + return VK_FORMAT_BC2_UNORM_BLOCK; + case Format::kBC3_UNORM: + return VK_FORMAT_BC3_UNORM_BLOCK; + case Format::kBC4_UNORM: + return VK_FORMAT_BC4_UNORM_BLOCK; + case Format::kBC5_UNORM: + return VK_FORMAT_BC5_UNORM_BLOCK; + default: + return VK_FORMAT_UNDEFINED; + } +} + +// Bytes per block (BC) / per texel (uncompressed) - the copy math table. +uint32_t FormatBytesPerBlock(Format format) { + switch (format) { + case Format::kBC1_UNORM: + case Format::kBC4_UNORM: + return 8; + case Format::kBC2_UNORM: + case Format::kBC3_UNORM: + case Format::kBC5_UNORM: + return 16; + case Format::kR8_UNORM: + return 1; + case Format::kR8G8_UNORM: + case Format::kB5G6R5_UNORM: + return 2; + case Format::kR8G8B8A8_UNORM: + case Format::kR8G8B8A8_UINT: + case Format::kR16G16_UNORM: + case Format::kR10G10B10A2_UNORM: + case Format::kR32_FLOAT: + case Format::kD32_FLOAT: + return 4; + case Format::kR32G32_FLOAT: + return 8; + case Format::kR32G32B32_FLOAT: + return 12; + case Format::kR32G32B32A32_FLOAT: + return 16; + default: + return 4; + } +} + +VkImageAspectFlags FormatAspect(Format format) { + return format == Format::kD32_FLOAT ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT; +} + +struct StateInfo { + VkPipelineStageFlags stage_mask; + VkAccessFlags access_mask; + VkImageLayout layout; +}; + +StateInfo ToStateInfo(ResourceState state) { + switch (state) { + case ResourceState::kRenderTarget: + return {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; + case ResourceState::kDepthWrite: + return {VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, + VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL}; + case ResourceState::kPixelShaderResource: + return {VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; + case ResourceState::kCopySource: + return {VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL}; + case ResourceState::kCopyDest: + return {VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL}; + case ResourceState::kGenericRead: + return {VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + VK_ACCESS_SHADER_READ_BIT, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; + case ResourceState::kGuestOutput: + // The presenter's internal steady state of the guest output image. + return {ui::vulkan::VulkanPresenter::kGuestOutputInternalStageMask, + ui::vulkan::VulkanPresenter::kGuestOutputInternalAccessMask, + ui::vulkan::VulkanPresenter::kGuestOutputInternalLayout}; + case ResourceState::kCommon: + default: + return {VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, VK_IMAGE_LAYOUT_GENERAL}; + } +} + +VkComponentSwizzle ToVkSwizzle(nrhi::Swizzle swizzle) { + switch (swizzle) { + case nrhi::Swizzle::kX: + return VK_COMPONENT_SWIZZLE_R; + case nrhi::Swizzle::kY: + return VK_COMPONENT_SWIZZLE_G; + case nrhi::Swizzle::kZ: + return VK_COMPONENT_SWIZZLE_B; + case nrhi::Swizzle::kW: + return VK_COMPONENT_SWIZZLE_A; + case nrhi::Swizzle::kZero: + return VK_COMPONENT_SWIZZLE_ZERO; + case nrhi::Swizzle::kOne: + default: + return VK_COMPONENT_SWIZZLE_ONE; + } +} + +VkBlendFactor ToVkBlend(nrhi::BlendFactor factor) { + switch (factor) { + case nrhi::BlendFactor::kZero: + return VK_BLEND_FACTOR_ZERO; + case nrhi::BlendFactor::kSrcAlpha: + return VK_BLEND_FACTOR_SRC_ALPHA; + case nrhi::BlendFactor::kInvSrcAlpha: + return VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + case nrhi::BlendFactor::kOne: + default: + return VK_BLEND_FACTOR_ONE; + } +} + +VkBlendOp ToVkBlendOp(nrhi::BlendOp op) { + return op == nrhi::BlendOp::kMin ? VK_BLEND_OP_MIN : VK_BLEND_OP_ADD; +} + +VkCompareOp ToVkCompare(nrhi::CompareFunc func) { + switch (func) { + case nrhi::CompareFunc::kLess: + return VK_COMPARE_OP_LESS; + case nrhi::CompareFunc::kLessEqual: + return VK_COMPARE_OP_LESS_OR_EQUAL; + case nrhi::CompareFunc::kAlways: + default: + return VK_COMPARE_OP_ALWAYS; + } +} + +// --------------------------------------------------------------------------- +// Handle types. +// --------------------------------------------------------------------------- + +class NrDeviceVulkan; + +class NrBufferVulkan : public nrhi::Buffer { + public: + uint64_t size() const override { return size_; } + + VkBuffer buffer = VK_NULL_HANDLE; + VmaAllocation allocation = VK_NULL_HANDLE; + void* mapping = nullptr; + uint64_t size_ = 0; // the app-requested size (allocation is padded) + HeapKind heap = HeapKind::kDefault; + bool device_local = false; // memory-type DEVICE_LOCAL (diagnostics) +}; + +// Live-buffer placement totals (bimodal-FPS diagnosis): bytes of nrhi +// buffers currently alive per (heap kind, device-local?) bucket. Updated at +// create/DestroyDeferred, reported in the periodic "nrhi-vulkan mem" line. +std::atomic g_buf_bytes_upload_dl{0}; +std::atomic g_buf_bytes_upload_host{0}; +std::atomic g_buf_bytes_default_dl{0}; +std::atomic g_buf_bytes_default_host{0}; + +class NrTextureVulkan : public nrhi::Texture { + public: + uint32_t width() const override { return desc.width; } + uint32_t height() const override { return desc.height; } + Format format() const override { return desc.format; } + + VkImage image = VK_NULL_HANDLE; + VmaAllocation allocation = VK_NULL_HANDLE; // VK_NULL_HANDLE for guest-output wrappers + nrhi::TextureDesc desc; + VkFormat vk_format = VK_FORMAT_UNDEFINED; + VkImageAspectFlags aspect = VK_IMAGE_ASPECT_COLOR_BIT; + // The current image layout; starts UNDEFINED so the first Barrier() + // transitions from UNDEFINED and discards, matching D3D12 initial-state + // semantics. Updated when barriers are queued and by internal transitions. + VkImageLayout current_layout = VK_IMAGE_LAYOUT_UNDEFINED; + // Mip-0 attachment view for RT/DS usage (identity swizzle - swizzled views + // are for sampling only), created lazily; for guest-output wrappers this + // is the presenter's own view and is not owned. + VkImageView attachment_view = VK_NULL_HANDLE; + bool attachment_view_owned = false; + bool is_guest_output = false; + // Latched pending clear, consumed as loadOp CLEAR at the next render pass + // begin targeting this texture (or flushed as an empty CLEAR pass). + bool pending_clear = false; + float clear_color[4] = {}; + float clear_depth = 1.0f; + + VkImageSubresourceRange WholeRange() const { + return ui::vulkan::util::InitializeSubresourceRange(aspect); + } +}; + +class NrTextureViewVulkan : public nrhi::TextureView { + public: + VkImageView view = VK_NULL_HANDLE; + NrTextureVulkan* texture = nullptr; +}; + +class NrBindingLayoutVulkan : public nrhi::BindingLayout { + public: + struct ParamInfo { + nrhi::BindingParamKind kind = nrhi::BindingParamKind::kTextureTable; + // Buffer kinds: binding index in set 0 (== index among buffer-kind + // params in param order, per the frozen derivation rule). + uint32_t set0_binding = 0; + VkDescriptorType descriptor_type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; + // Table kinds: the descriptor set index (1 + index among table params in + // param order) and the declared table size. + uint32_t set_index = 0; + uint32_t table_index = 0; // index among table params + uint32_t table_size = 0; + }; + + VkPipelineLayout pipeline_layout = VK_NULL_HANDLE; + VkDescriptorSetLayout set0_layout = VK_NULL_HANDLE; + std::vector table_layouts; // one per kTextureTable param + std::vector immutable_samplers; // owned + ParamInfo params[nrhi::kMaxBindingParams] = {}; + uint32_t param_count = 0; + uint32_t buffer_binding_count = 0; // dynamic-offset bindings in set 0 + uint32_t table_count = 0; + int32_t constants_param = -1; // param index of the kConstants param + uint32_t constants_size_bytes = 0; // count * 4 from the desc +}; + +class NrShaderVulkan : public nrhi::Shader { + public: + VkShaderModule module = VK_NULL_HANDLE; + nrhi::ShaderStage stage = nrhi::ShaderStage::kVertex; + // dxc -spirv keeps the HLSL entry-point name as the SPIR-V OpEntryPoint + // name (unlike glslang's "main"); pipelines must pass it as pName. + std::string entry_point = "main"; +}; + +class NrPipelineVulkan : public nrhi::Pipeline { + public: + VkPipeline list_pipeline = VK_NULL_HANDLE; + // TRIANGLE_STRIP twin, created lazily on the first strip draw with this + // pipeline bound. Requires the shader modules (below) to still be alive at + // that point - the scene keeps its shaders for the process lifetime, + // mirroring how D3D12 baked the blobs into the PSO at creation. + VkPipeline strip_pipeline = VK_NULL_HANDLE; + + // Everything needed to build the strip twin. + VkShaderModule vs_module = VK_NULL_HANDLE; + VkShaderModule ps_module = VK_NULL_HANDLE; + // dxc-produced SPIR-V keeps the HLSL entry-point names. + std::string vs_entry = "main"; + std::string ps_entry = "main"; + VkPipelineLayout pipeline_layout = VK_NULL_HANDLE; + std::vector input_elements; + uint32_t vertex_stride = 0; + nrhi::BlendStateDesc blend; + nrhi::DepthStateDesc depth; + nrhi::CullMode cull = nrhi::CullMode::kNone; + bool depth_clip = true; + Format rtv_format = Format::kUnknown; + Format dsv_format = Format::kUnknown; + uint32_t sample_count = 1; +}; + +// --------------------------------------------------------------------------- +// Frame command recording. +// --------------------------------------------------------------------------- + +// Per-frame render-thread CPU accounting for the backend's own work (the +// app's timers cover RenderScene minus this layer; sustained multi-ms frames +// here showed up as the "render=13ms, items=0.6ms" gap). Reported from +// EndFrame when a frame exceeds the threshold, throttled. +struct NrFrameProf { + struct Bucket { + uint64_t us = 0; + uint32_t count = 0; + }; + Bucket pass_open; + Bucket flush_barriers; + Bucket copies; + Bucket table_miss; + Bucket set0_miss; + Bucket view_destroy; + Bucket view_create; + Bucket const_slice; + Bucket pipeline_build; + void Reset() { *this = NrFrameProf{}; } + uint64_t Total() const { + return pass_open.us + flush_barriers.us + copies.us + table_miss.us + set0_miss.us + + view_destroy.us + view_create.us + const_slice.us + pipeline_build.us; + } +}; + +class NrProfScope { + public: + NrProfScope(NrFrameProf::Bucket& bucket) + : bucket_(bucket), start_(std::chrono::steady_clock::now()) {} + ~NrProfScope() { + bucket_.us += uint64_t(std::chrono::duration_cast( + std::chrono::steady_clock::now() - start_) + .count()); + ++bucket_.count; + } + + private: + NrFrameProf::Bucket& bucket_; + std::chrono::steady_clock::time_point start_; +}; + +class NrCmdVulkan : public nrhi::Cmd { + public: + void SetBindingLayout(nrhi::BindingLayout* layout) override; + void SetPipeline(nrhi::Pipeline* pipeline) override; + void SetRootConstants(uint32_t param, uint32_t count, const void* values, + uint32_t dest_offset_in_values) override; + void SetConstantBuffer(uint32_t param, nrhi::Buffer* buffer, uint64_t offset) override; + void SetBufferSrv(uint32_t param, nrhi::Buffer* buffer, uint64_t offset) override; + void SetTexture(uint32_t param, nrhi::TextureView* view) override; + void SetTexturePair(uint32_t param, nrhi::TextureView* first, + nrhi::TextureView* second) override; + void SetTextures(uint32_t param, nrhi::TextureView* const* views, uint32_t count) override; + void SetRenderTargets(nrhi::Texture* color, nrhi::Texture* depth) override; + void ClearRenderTarget(nrhi::Texture* color, const float color4[4]) override; + void ClearDepth(nrhi::Texture* depth, float value) override; + void SetViewport(const nrhi::Viewport& viewport) override; + void SetScissor(const nrhi::Rect& rect) override; + void SetVertexBuffer(nrhi::Buffer* buffer, uint64_t offset, uint32_t size_bytes, + uint32_t stride) override; + void SetIndexBuffer(nrhi::Buffer* buffer, uint64_t offset, uint32_t size_bytes) override; + void SetPrimitiveTopology(nrhi::PrimitiveTopology topology) override; + void Draw(uint32_t vertex_count, uint32_t start_vertex) override; + void DrawIndexed(uint32_t index_count, uint32_t start_index, int32_t base_vertex) override; + void CopyBufferToTexture(nrhi::Texture* dst, uint32_t mip, uint32_t array_slice, + nrhi::Buffer* src, uint64_t src_offset, uint32_t row_pitch, + uint32_t width, uint32_t height, uint32_t depth) override; + void CopyTextureToBuffer(nrhi::Buffer* dst, uint64_t dst_offset, uint32_t row_pitch, + nrhi::Texture* src, uint32_t mip, uint32_t width, + uint32_t height) override; + void Barrier(nrhi::Texture* texture, ResourceState before, ResourceState after) override; + void FlushBarriers() override; + + // --- frame handling (driven by the device) --- + void ResetFrameState(); + void EndFrame(); + + NrDeviceVulkan* device = nullptr; + + private: + friend class NrDeviceVulkan; + + bool EnsureRenderPassOpen(); + void EndRenderPassIfOpen(); + bool EnsureDrawState(); + void LatchPendingClear(NrTextureVulkan* texture, const float* color4, float depth); + + // Latched state (D3D12 command-list semantics). + NrBindingLayoutVulkan* layout_ = nullptr; + NrPipelineVulkan* pipeline_ = nullptr; + VkPipeline bound_pipeline_ = VK_NULL_HANDLE; + nrhi::PrimitiveTopology topology_ = nrhi::PrimitiveTopology::kTriangleList; + + // Root constants: CPU shadow block, copied to a ring UBO slice when dirty. + uint32_t constants_shadow_[64] = {}; + bool constants_dirty_ = false; + uint32_t constants_ring_offset_ = ~0u; // dynamic offset of the current slice + + // Set 0 buffer bindings, by set-0 binding index. + NrBufferVulkan* set0_buffers_[nrhi::kMaxBindingParams] = {}; + uint32_t set0_offsets_[nrhi::kMaxBindingParams] = {}; + bool set0_rebind_needed_ = true; + VkDescriptorSet set0_bound_ = VK_NULL_HANDLE; + + // Texture tables, by table index within the layout. + NrTextureViewVulkan* table_views_[nrhi::kMaxBindingParams][nrhi::kMaxTextureTableSize] = {}; + bool table_dirty_[nrhi::kMaxBindingParams] = {}; + + // Render target state. + NrTextureVulkan* rt_color_ = nullptr; + NrTextureVulkan* rt_depth_ = nullptr; + bool render_pass_open_ = false; +}; + +// --------------------------------------------------------------------------- +// Device. +// --------------------------------------------------------------------------- + +class NrDeviceVulkan : public nrhi::Device { + public: + // Every kUpload/kDefault buffer allocation (and the ring UBO) is padded by + // this much slack: dynamic-offset descriptors are written with fixed + // ranges (UBO 16384, SSBO 65536), so an offset near the end of the app's + // logical buffer must still leave the (offset + range) window in-bounds. + static constexpr uint64_t kBufferPadding = 65536; + static constexpr VkDeviceSize kUboBindRange = 16384; // minimum maxUniformBufferRange + static constexpr VkDeviceSize kSsboBindRange = 65536; // covered by kBufferPadding + + static constexpr uint32_t kRingRegions = 8; + static constexpr uint32_t kRingRegionSize = 1024 * 1024; + + explicit NrDeviceVulkan(VulkanCommandProcessor* cp) + : cp_(cp), vulkan_device_(cp->GetVulkanDevice()) { + cmd_.device = this; + // Thread-safe allocator: nrhi creation APIs are called from decode + // worker threads concurrently with the render thread. + allocator_ = ui::vulkan::CreateVmaAllocator(vulkan_device_, /*externally_synchronized=*/false); + + // The internal root-constant ring UBO: kRingRegions regions cycled by + // frame index (deeper than the CP's 3 frames in flight), persistently + // mapped, host-coherent. +kBufferPadding for the fixed descriptor range. + VkBufferCreateInfo ring_info = {}; + ring_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + ring_info.size = VkDeviceSize(kRingRegions) * kRingRegionSize + kBufferPadding; + // STORAGE too: unbound kBufferSrv params fall back to this buffer so + // their STORAGE_BUFFER_DYNAMIC descriptors are always valid. + ring_info.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; + ring_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + VmaAllocationCreateInfo ring_alloc_info = {}; + ring_alloc_info.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT | + VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT; + ring_alloc_info.usage = VMA_MEMORY_USAGE_AUTO; + ring_alloc_info.requiredFlags = + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + VmaAllocationInfo ring_result_info = {}; + if (allocator_ == VK_NULL_HANDLE || + vmaCreateBuffer(allocator_, &ring_info, &ring_alloc_info, &ring_buffer_, + &ring_allocation_, &ring_result_info) != VK_SUCCESS) { + REXLOG_ERROR("nrhi-vulkan: root-constant ring UBO creation failed"); + ring_buffer_ = VK_NULL_HANDLE; + } else { + ring_mapping_ = static_cast(ring_result_info.pMappedData); + } + } + + ~NrDeviceVulkan() override { + // Callers guarantee GPU idle; release everything immediately. + const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device_->functions(); + const VkDevice device = vulkan_device_->device(); + DrainRetired(~0ull); + for (auto& entry : set0_sets_) { + dfn.vkFreeDescriptorSets(device, entry.second.pool, 1, &entry.second.set); + } + for (auto& entry : table_sets_) { + dfn.vkFreeDescriptorSets(device, entry.second.pool, 1, &entry.second.set); + } + for (auto& entry : framebuffers_) { + dfn.vkDestroyFramebuffer(device, entry.second, nullptr); + } + for (auto& entry : render_passes_) { + dfn.vkDestroyRenderPass(device, entry.second, nullptr); + } + for (VkDescriptorPool pool : descriptor_pools_) { + dfn.vkDestroyDescriptorPool(device, pool, nullptr); + } + // App-held objects that were never DestroyDeferred'ed (the scene keeps + // its caches for the whole session and gets no teardown notification): + // release them here so no child object outlives vkDestroyDevice. + // Framebuffers referencing leftover views were destroyed above; order + // below: views before their images. + if (!live_buffers_.empty() || !live_textures_.empty() || !live_views_.empty() || + !live_pipelines_.empty() || !live_shaders_.empty()) { + REXLOG_INFO( + "nrhi-vulkan: releasing app-held objects at device destruction " + "(buffers={} textures={} views={} pipelines={} shaders={})", + live_buffers_.size(), live_textures_.size(), live_views_.size(), + live_pipelines_.size(), live_shaders_.size()); + } + for (NrTextureViewVulkan* v : live_views_) { + if (v->view != VK_NULL_HANDLE) { + dfn.vkDestroyImageView(device, v->view, nullptr); + } + delete v; + } + live_views_.clear(); + for (NrPipelineVulkan* p : live_pipelines_) { + if (p->list_pipeline != VK_NULL_HANDLE) { + dfn.vkDestroyPipeline(device, p->list_pipeline, nullptr); + } + if (p->strip_pipeline != VK_NULL_HANDLE) { + dfn.vkDestroyPipeline(device, p->strip_pipeline, nullptr); + } + delete p; + } + live_pipelines_.clear(); + for (NrShaderVulkan* s : live_shaders_) { + if (s->module != VK_NULL_HANDLE) { + dfn.vkDestroyShaderModule(device, s->module, nullptr); + } + delete s; + } + live_shaders_.clear(); + for (NrTextureVulkan* t : live_textures_) { + if (t->attachment_view_owned && t->attachment_view != VK_NULL_HANDLE) { + dfn.vkDestroyImageView(device, t->attachment_view, nullptr); + } + if (t->image != VK_NULL_HANDLE && t->allocation != VK_NULL_HANDLE) { + vmaDestroyImage(allocator_, t->image, t->allocation); + } + delete t; + } + live_textures_.clear(); + for (NrBufferVulkan* b : live_buffers_) { + if (b->buffer != VK_NULL_HANDLE) { + vmaDestroyBuffer(allocator_, b->buffer, b->allocation); + } + delete b; + } + live_buffers_.clear(); + for (NrBindingLayoutVulkan* layout : layouts_) { + for (VkSampler sampler : layout->immutable_samplers) { + dfn.vkDestroySampler(device, sampler, nullptr); + } + for (VkDescriptorSetLayout set_layout : layout->table_layouts) { + dfn.vkDestroyDescriptorSetLayout(device, set_layout, nullptr); + } + if (layout->set0_layout != VK_NULL_HANDLE) { + dfn.vkDestroyDescriptorSetLayout(device, layout->set0_layout, nullptr); + } + if (layout->pipeline_layout != VK_NULL_HANDLE) { + dfn.vkDestroyPipelineLayout(device, layout->pipeline_layout, nullptr); + } + delete layout; + } + for (auto& entry : guest_outputs_) { + delete entry.second; // image + view owned by the presenter + } + if (white_view_ != VK_NULL_HANDLE) { + dfn.vkDestroyImageView(device, white_view_, nullptr); + } + if (white_image_ != VK_NULL_HANDLE) { + vmaDestroyImage(allocator_, white_image_, white_allocation_); + } + if (white_staging_ != VK_NULL_HANDLE) { + vmaDestroyBuffer(allocator_, white_staging_, white_staging_allocation_); + } + if (ring_buffer_ != VK_NULL_HANDLE) { + vmaDestroyBuffer(allocator_, ring_buffer_, ring_allocation_); + } + if (allocator_ != VK_NULL_HANDLE) { + vmaDestroyAllocator(allocator_); + } + } + + Backend backend() const override { return Backend::kVulkan; } + + // --- thread-safe creation / mapping --- + + nrhi::Buffer* CreateBuffer(const nrhi::BufferDesc& desc) override { + VkBufferCreateInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + VmaAllocationCreateInfo alloc_info = {}; + switch (desc.heap) { + case HeapKind::kUpload: + info.size = desc.size + kBufferPadding; + info.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | + VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | + VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; + alloc_info.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT | + VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT; + alloc_info.usage = VMA_MEMORY_USAGE_AUTO; + // Coherent is required, not merely preferred: the app writes upload + // memory with no flush machinery (write-combined D3D12 semantics). + // Every conformant implementation has a HOST_VISIBLE|HOST_COHERENT + // type. + alloc_info.requiredFlags = + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + break; + case HeapKind::kReadback: + info.size = desc.size; + info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; + alloc_info.flags = + VMA_ALLOCATION_CREATE_MAPPED_BIT | VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT; + alloc_info.usage = VMA_MEMORY_USAGE_AUTO; + alloc_info.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT; + alloc_info.preferredFlags = VK_MEMORY_PROPERTY_HOST_CACHED_BIT; + break; + case HeapKind::kDefault: + default: + info.size = desc.size + kBufferPadding; + info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | + VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | + VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; + alloc_info.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE; + break; + } + VkBuffer buffer = VK_NULL_HANDLE; + VmaAllocation allocation = VK_NULL_HANDLE; + VmaAllocationInfo result_info = {}; + if (vmaCreateBuffer(allocator_, &info, &alloc_info, &buffer, &allocation, &result_info) != + VK_SUCCESS) { + REXLOG_ERROR("nrhi-vulkan: buffer creation failed ({} bytes, heap {})", desc.size, + uint32_t(desc.heap)); + return nullptr; + } + VkMemoryPropertyFlags mem_flags = 0; + vmaGetAllocationMemoryProperties(allocator_, allocation, &mem_flags); + const bool mem_dl = (mem_flags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0; + if (desc.heap == HeapKind::kDefault && !mem_dl) { + REXLOG_WARN("nrhi-vulkan: DEFAULT-heap buffer ({} bytes) landed in HOST memory (flags {:#x})", + desc.size, uint32_t(mem_flags)); + } + if (desc.heap == HeapKind::kUpload) { + (mem_dl ? g_buf_bytes_upload_dl : g_buf_bytes_upload_host) + .fetch_add(desc.size, std::memory_order_relaxed); + } else if (desc.heap == HeapKind::kDefault) { + (mem_dl ? g_buf_bytes_default_dl : g_buf_bytes_default_host) + .fetch_add(desc.size, std::memory_order_relaxed); + } + auto* b = new NrBufferVulkan(); + b->buffer = buffer; + b->allocation = allocation; + b->mapping = result_info.pMappedData; // persistent map for host heaps + b->size_ = desc.size; + b->heap = desc.heap; + b->device_local = mem_dl; + { + std::lock_guard lock(mutex_); + live_buffers_.insert(b); + } + return b; + } + + nrhi::Texture* CreateTexture(const nrhi::TextureDesc& desc) override { + VkImageCreateInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + info.imageType = desc.kind == TextureKind::k3D ? VK_IMAGE_TYPE_3D : VK_IMAGE_TYPE_2D; + info.format = ToVkFormat(desc.format); + info.extent.width = desc.width; + info.extent.height = desc.height; + info.extent.depth = desc.kind == TextureKind::k3D ? desc.depth : 1; + info.mipLevels = desc.mip_levels; + info.arrayLayers = desc.kind == TextureKind::kCube ? 6 : 1; + info.samples = VkSampleCountFlagBits(desc.sample_count); + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + if (desc.usage & nrhi::kTextureUsageRenderTarget) { + info.usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + } + if (desc.usage & nrhi::kTextureUsageDepthStencil) { + info.usage |= VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; + } + if (desc.usage & nrhi::kTextureUsageCopySource) { + info.usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + } + if (desc.kind == TextureKind::kCube) { + info.flags |= VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT; + } + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + VmaAllocationCreateInfo alloc_info = {}; + alloc_info.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE; + VkImage image = VK_NULL_HANDLE; + VmaAllocation allocation = VK_NULL_HANDLE; + if (vmaCreateImage(allocator_, &info, &alloc_info, &image, &allocation, nullptr) != + VK_SUCCESS) { + REXLOG_ERROR("nrhi-vulkan: texture creation failed ({}x{} fmt {})", desc.width, desc.height, + uint32_t(desc.format)); + return nullptr; + } + { + VkMemoryPropertyFlags mem_flags = 0; + vmaGetAllocationMemoryProperties(allocator_, allocation, &mem_flags); + if (!(mem_flags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)) { + REXLOG_WARN( + "nrhi-vulkan: texture ({}x{} fmt {} samples {}) landed in HOST memory (flags {:#x})", + desc.width, desc.height, uint32_t(desc.format), desc.sample_count, + uint32_t(mem_flags)); + } + } + auto* t = new NrTextureVulkan(); + t->image = image; + t->allocation = allocation; + t->desc = desc; + t->vk_format = info.format; + t->aspect = FormatAspect(desc.format); + t->attachment_view_owned = true; + { + std::lock_guard lock(mutex_); + live_textures_.insert(t); + } + // current_layout starts UNDEFINED regardless of desc.initial_state: the + // first transition (explicit Barrier or the internal ensure-layout on + // first attachment/copy use) comes from UNDEFINED and discards. + return t; + } + + void* Map(nrhi::Buffer* buffer) override { + // Host-heap buffers are persistently mapped at creation. + return static_cast(buffer)->mapping; + } + + void Unmap(nrhi::Buffer*) override { + // Persistent mapping - nothing to do. + } + + void InvalidateForRead(nrhi::Buffer* buffer, uint64_t offset, uint64_t size) override { + auto* b = static_cast(buffer); + vmaInvalidateAllocation(allocator_, b->allocation, offset, size); + } + + // --- deferred destruction --- + + void DestroyDeferred(nrhi::Buffer* buffer) override { + if (buffer == nullptr) return; + auto* b = static_cast(buffer); + // Retire cached set-0 descriptor sets whose buffer tuple references this + // buffer. set0_sets_ is render-thread-only, like the D3D12 backend's + // bindings_: buffer destruction of bound buffers happens on the render + // thread. + const uint64_t submission = cp_->GetCurrentSubmission(); + for (auto it = set0_sets_.begin(); it != set0_sets_.end();) { + bool references = false; + for (uint32_t i = 0; i < it->first.count; ++i) { + if (it->first.buffers[i] == b->buffer) { + references = true; + break; + } + } + if (references) { + std::lock_guard lock(mutex_); + RetireDescriptorSetLocked(it->second, submission); + it = set0_sets_.erase(it); + } else { + ++it; + } + } + if (b->heap == HeapKind::kUpload) { + (b->device_local ? g_buf_bytes_upload_dl : g_buf_bytes_upload_host) + .fetch_sub(b->size_, std::memory_order_relaxed); + } else if (b->heap == HeapKind::kDefault) { + (b->device_local ? g_buf_bytes_default_dl : g_buf_bytes_default_host) + .fetch_sub(b->size_, std::memory_order_relaxed); + } + std::lock_guard lock(mutex_); + live_buffers_.erase(b); + RetiredObject r; + r.submission = submission; + r.buffer = b->buffer; + r.buffer_allocation = b->allocation; + retired_.push_back(r); + delete b; + } + + void DestroyDeferred(nrhi::Texture* texture) override { + if (texture == nullptr) return; + auto* t = static_cast(texture); + if (t->is_guest_output) return; // wrappers are owned by the device cache + RetireFramebuffersForView(t->attachment_view); + if (cmd_.rt_color_ == t) cmd_.rt_color_ = nullptr; + if (cmd_.rt_depth_ == t) cmd_.rt_depth_ = nullptr; + std::erase(pending_clear_textures_, t); + std::lock_guard lock(mutex_); + live_textures_.erase(t); + RetiredObject r; + r.submission = cp_->GetCurrentSubmission(); + r.image = t->image; + r.image_allocation = t->allocation; + if (t->attachment_view_owned) { + r.view = t->attachment_view; + } + retired_.push_back(r); + delete t; + } + + void DestroyDeferred(nrhi::TextureView* view) override { + if (view == nullptr) return; + NrProfScope prof_scope(prof_.view_destroy); + auto* v = static_cast(view); + const uint64_t submission = cp_->GetCurrentSubmission(); + // Retire every cached texture descriptor set this view participates in + // (render-thread-only cache, mirroring the D3D12 backend's bindings_). + for (auto it = table_sets_.begin(); it != table_sets_.end();) { + bool references = false; + for (uint32_t i = 0; i < it->first.count; ++i) { + if (it->first.views[i] == v) { + references = true; + break; + } + } + if (references) { + std::lock_guard lock(mutex_); + RetireDescriptorSetLocked(it->second, submission); + it = table_sets_.erase(it); + } else { + ++it; + } + } + std::lock_guard lock(mutex_); + live_views_.erase(v); + RetiredObject r; + r.submission = submission; + r.view = v->view; + retired_.push_back(r); + delete v; + } + + void DestroyDeferred(nrhi::Pipeline* pipeline) override { + if (pipeline == nullptr) return; + auto* p = static_cast(pipeline); + std::lock_guard lock(mutex_); + live_pipelines_.erase(p); + RetiredObject r; + r.submission = cp_->GetCurrentSubmission(); + r.pipelines[0] = p->list_pipeline; + r.pipelines[1] = p->strip_pipeline; + retired_.push_back(r); + delete p; + } + + void DestroyDeferred(nrhi::Shader* shader) override { + if (shader == nullptr) return; + auto* s = static_cast(shader); + std::lock_guard lock(mutex_); + live_shaders_.erase(s); + RetiredObject r; + r.submission = cp_->GetCurrentSubmission(); + r.shader_module = s->module; + retired_.push_back(r); + delete s; + } + + // --- render-thread-only creation --- + + nrhi::TextureView* CreateTextureView(nrhi::Texture* texture, + const nrhi::TextureViewDesc& desc) override { + NrProfScope prof_scope(prof_.view_create); + auto* t = static_cast(texture); + VkImageViewCreateInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + info.image = t->image; + switch (desc.dimension) { + case nrhi::ViewDimension::kCube: + info.viewType = VK_IMAGE_VIEW_TYPE_CUBE; + break; + case nrhi::ViewDimension::k3D: + info.viewType = VK_IMAGE_VIEW_TYPE_3D; + break; + case nrhi::ViewDimension::k2DMS: + case nrhi::ViewDimension::k2D: + default: + info.viewType = VK_IMAGE_VIEW_TYPE_2D; + break; + } + // kUnknown = the texture's own format. D32 depth is sampled through the + // depth aspect of the D32_SFLOAT image (the shader reads .r - the + // R32_FLOAT-cast semantics of the D3D12 arrangement). + info.format = desc.format != Format::kUnknown ? ToVkFormat(desc.format) : t->vk_format; + info.components.r = ToVkSwizzle(desc.swizzle[0]); + info.components.g = ToVkSwizzle(desc.swizzle[1]); + info.components.b = ToVkSwizzle(desc.swizzle[2]); + info.components.a = ToVkSwizzle(desc.swizzle[3]); + info.subresourceRange.aspectMask = t->aspect; + info.subresourceRange.baseMipLevel = desc.base_mip; + info.subresourceRange.levelCount = + desc.mip_levels == ~0u ? VK_REMAINING_MIP_LEVELS : desc.mip_levels; + info.subresourceRange.baseArrayLayer = 0; + info.subresourceRange.layerCount = VK_REMAINING_ARRAY_LAYERS; + const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device_->functions(); + VkImageView view = VK_NULL_HANDLE; + if (dfn.vkCreateImageView(vulkan_device_->device(), &info, nullptr, &view) != VK_SUCCESS) { + REXLOG_ERROR("nrhi-vulkan: texture view creation failed"); + return nullptr; + } + auto* v = new NrTextureViewVulkan(); + v->view = view; + v->texture = t; + { + std::lock_guard lock(mutex_); + live_views_.insert(v); + } + return v; + } + + nrhi::BindingLayout* CreateBindingLayout(const nrhi::BindingLayoutDesc& desc) override { + const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device_->functions(); + const VkDevice device = vulkan_device_->device(); + const ui::vulkan::VulkanDevice::Properties& props = vulkan_device_->properties(); + + auto* layout = new NrBindingLayoutVulkan(); + layout->param_count = desc.param_count; + + // Immutable samplers from the static sampler descs. + std::vector samplers; + for (uint32_t i = 0; i < desc.static_sampler_count; ++i) { + const nrhi::StaticSamplerDesc& s = desc.static_samplers[i]; + VkSamplerCreateInfo sampler_info = {}; + sampler_info.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + const bool linear = s.filter != nrhi::Filter::kPoint; + sampler_info.magFilter = linear ? VK_FILTER_LINEAR : VK_FILTER_NEAREST; + sampler_info.minFilter = sampler_info.magFilter; + sampler_info.mipmapMode = + linear ? VK_SAMPLER_MIPMAP_MODE_LINEAR : VK_SAMPLER_MIPMAP_MODE_NEAREST; + VkSamplerAddressMode address = s.address == nrhi::AddressMode::kWrap + ? VK_SAMPLER_ADDRESS_MODE_REPEAT + : VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + sampler_info.addressModeU = address; + sampler_info.addressModeV = address; + sampler_info.addressModeW = address; + if (s.filter == nrhi::Filter::kAnisotropic && props.samplerAnisotropy) { + sampler_info.anisotropyEnable = VK_TRUE; + sampler_info.maxAnisotropy = + std::min(float(std::max(s.max_anisotropy, 1u)), props.maxSamplerAnisotropy); + } + sampler_info.minLod = 0.0f; + sampler_info.maxLod = VK_LOD_CLAMP_NONE; + VkSampler sampler = VK_NULL_HANDLE; + if (dfn.vkCreateSampler(device, &sampler_info, nullptr, &sampler) != VK_SUCCESS) { + REXLOG_ERROR("nrhi-vulkan: immutable sampler creation failed"); + } + samplers.push_back(sampler); + } + layout->immutable_samplers = samplers; + + // Set 0: buffer params in param order (dynamic-offset descriptors), then + // the static samplers in declaration order (immutable). All graphics + // stage flags everywhere - the frozen derivation rule. + const VkShaderStageFlags kAllGraphics = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; + std::vector set0_bindings; + uint32_t table_count = 0; + for (uint32_t i = 0; i < desc.param_count; ++i) { + const nrhi::BindingParamDesc& p = desc.params[i]; + NrBindingLayoutVulkan::ParamInfo& info = layout->params[i]; + info.kind = p.kind; + switch (p.kind) { + case nrhi::BindingParamKind::kConstants: + layout->constants_param = int32_t(i); + layout->constants_size_bytes = p.count * sizeof(uint32_t); + [[fallthrough]]; + case nrhi::BindingParamKind::kConstantBuffer: + case nrhi::BindingParamKind::kBufferSrv: { + info.set0_binding = uint32_t(set0_bindings.size()); + info.descriptor_type = p.kind == nrhi::BindingParamKind::kBufferSrv + ? VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC + : VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; + VkDescriptorSetLayoutBinding binding = {}; + binding.binding = info.set0_binding; + binding.descriptorType = info.descriptor_type; + binding.descriptorCount = 1; + binding.stageFlags = kAllGraphics; + set0_bindings.push_back(binding); + break; + } + case nrhi::BindingParamKind::kTextureTable: { + info.table_index = table_count; + info.set_index = 1 + table_count; + info.table_size = p.count; + ++table_count; + break; + } + } + } + layout->buffer_binding_count = uint32_t(set0_bindings.size()); + layout->table_count = table_count; + for (uint32_t i = 0; i < desc.static_sampler_count; ++i) { + VkDescriptorSetLayoutBinding binding = {}; + binding.binding = layout->buffer_binding_count + i; + binding.descriptorType = VK_DESCRIPTOR_TYPE_SAMPLER; + binding.descriptorCount = 1; + binding.stageFlags = kAllGraphics; + binding.pImmutableSamplers = &layout->immutable_samplers[i]; + set0_bindings.push_back(binding); + } + + VkDescriptorSetLayoutCreateInfo set_layout_info = {}; + set_layout_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + set_layout_info.bindingCount = uint32_t(set0_bindings.size()); + set_layout_info.pBindings = set0_bindings.data(); + if (dfn.vkCreateDescriptorSetLayout(device, &set_layout_info, nullptr, &layout->set0_layout) != + VK_SUCCESS) { + REXLOG_ERROR("nrhi-vulkan: set-0 descriptor set layout creation failed"); + } + + // Sets 1..N: one per kTextureTable param, SAMPLED_IMAGE bindings + // 0..count-1. + for (uint32_t i = 0; i < desc.param_count; ++i) { + const nrhi::BindingParamDesc& p = desc.params[i]; + if (p.kind != nrhi::BindingParamKind::kTextureTable) continue; + std::vector table_bindings(p.count); + for (uint32_t j = 0; j < p.count; ++j) { + table_bindings[j] = {}; + table_bindings[j].binding = j; + table_bindings[j].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + table_bindings[j].descriptorCount = 1; + table_bindings[j].stageFlags = kAllGraphics; + } + VkDescriptorSetLayoutCreateInfo table_layout_info = {}; + table_layout_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + table_layout_info.bindingCount = p.count; + table_layout_info.pBindings = table_bindings.data(); + VkDescriptorSetLayout table_layout = VK_NULL_HANDLE; + if (dfn.vkCreateDescriptorSetLayout(device, &table_layout_info, nullptr, &table_layout) != + VK_SUCCESS) { + REXLOG_ERROR("nrhi-vulkan: texture-table descriptor set layout creation failed"); + } + layout->table_layouts.push_back(table_layout); + } + + std::vector all_layouts; + all_layouts.push_back(layout->set0_layout); + for (VkDescriptorSetLayout l : layout->table_layouts) { + all_layouts.push_back(l); + } + VkPipelineLayoutCreateInfo pipeline_layout_info = {}; + pipeline_layout_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + pipeline_layout_info.setLayoutCount = uint32_t(all_layouts.size()); + pipeline_layout_info.pSetLayouts = all_layouts.data(); + if (dfn.vkCreatePipelineLayout(device, &pipeline_layout_info, nullptr, + &layout->pipeline_layout) != VK_SUCCESS) { + REXLOG_ERROR("nrhi-vulkan: pipeline layout creation failed"); + layouts_.push_back(layout); + return nullptr; + } + layouts_.push_back(layout); + return layout; + } + + nrhi::Shader* CreateShader(const nrhi::ShaderDesc& desc) override { + if (desc.spirv == nullptr || desc.spirv_size_bytes == 0) { + REXLOG_ERROR("nrhi-vulkan: no SPIR-V blob for {}:{}", + desc.name != nullptr ? desc.name : "?", + desc.entry_point != nullptr ? desc.entry_point : "?"); + return nullptr; + } + VkShaderModule module = + ui::vulkan::util::CreateShaderModule(vulkan_device_, desc.spirv, desc.spirv_size_bytes); + if (module == VK_NULL_HANDLE) { + REXLOG_ERROR("nrhi-vulkan: shader module creation failed ({}:{})", + desc.name != nullptr ? desc.name : "?", + desc.entry_point != nullptr ? desc.entry_point : "?"); + return nullptr; + } + auto* shader = new NrShaderVulkan(); + shader->module = module; + shader->stage = desc.stage; + if (desc.entry_point != nullptr) { + shader->entry_point = desc.entry_point; + } + { + std::lock_guard lock(mutex_); + live_shaders_.insert(shader); + } + return shader; + } + + nrhi::Pipeline* CreateGraphicsPipeline(const nrhi::GraphicsPipelineDesc& desc) override { + auto* layout = static_cast(desc.layout); + auto* vs = static_cast(desc.vs); + auto* ps = static_cast(desc.ps); + if (layout == nullptr || vs == nullptr || ps == nullptr) return nullptr; + auto* pipeline = new NrPipelineVulkan(); + pipeline->vs_module = vs->module; + pipeline->ps_module = ps->module; + pipeline->vs_entry = vs->entry_point; + pipeline->ps_entry = ps->entry_point; + pipeline->pipeline_layout = layout->pipeline_layout; + if (desc.input_elements != nullptr && desc.input_element_count != 0) { + pipeline->input_elements.assign(desc.input_elements, + desc.input_elements + desc.input_element_count); + } + pipeline->vertex_stride = desc.vertex_stride; + pipeline->blend = desc.blend; + pipeline->depth = desc.depth; + pipeline->cull = desc.cull; + pipeline->depth_clip = desc.depth_clip; + pipeline->rtv_format = desc.rtv_format; + pipeline->dsv_format = desc.dsv_format; + pipeline->sample_count = desc.sample_count; + pipeline->list_pipeline = BuildPipeline(*pipeline, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST); + if (pipeline->list_pipeline == VK_NULL_HANDLE) { + REXLOG_ERROR("nrhi-vulkan: graphics pipeline creation failed"); + delete pipeline; + return nullptr; + } + // The strip twin is built EAGERLY: the scene destroys its shaders right + // after pipeline creation (mirroring the D3D12 blob release), so a lazy + // first-strip-draw build would reference freed VkShaderModules (this was + // a driver crash on gameplay load). + pipeline->strip_pipeline = BuildPipeline(*pipeline, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP); + if (pipeline->strip_pipeline == VK_NULL_HANDLE) { + REXLOG_ERROR("nrhi-vulkan: strip pipeline creation failed"); + DestroyDeferred(static_cast(pipeline)); + return nullptr; + } + { + std::lock_guard lock(mutex_); + live_pipelines_.insert(pipeline); + } + return pipeline; + } + + uint32_t GetSupportedSampleCount(Format format, uint32_t desired) override { + (void)format; // color RT formats only; the device-wide framebuffer + + // sampled-image color masks are the portable bound. + const ui::vulkan::VulkanDevice::Properties& props = vulkan_device_->properties(); + const VkSampleCountFlags mask = + props.framebufferColorSampleCounts & props.sampledImageColorSampleCounts; + uint32_t count = desired; + while (count > 1 && !(mask & count)) { + count >>= 1; + } + return std::max(count, 1u); + } + + uint64_t CurrentSubmission() const override { return cp_->GetCurrentSubmission(); } + uint64_t CompletedSubmission() const override { return cp_->GetCompletedSubmission(); } + + // --- frame handling (called from the command processor) --- + + nrhi::Cmd* BeginFrame(VkImage guest_output_image, VkImageView guest_output_image_view, + bool guest_output_ever_written, uint32_t width, uint32_t height, + nrhi::Texture** guest_output_out) { + prof_.Reset(); + { + std::lock_guard lock(mutex_); + DrainRetired(cp_->GetCompletedSubmission()); + } + ++frame_index_; + if ((frame_index_ % 600) == 0) { + // Periodic VRAM budget attribution (bimodal-FPS diagnosis): log every + // heap's VMA usage vs the driver-reported budget, flagging device-local. + VmaBudget budgets[VK_MAX_MEMORY_HEAPS] = {}; + vmaGetHeapBudgets(allocator_, budgets); + const VkPhysicalDeviceMemoryProperties* mem_props = nullptr; + vmaGetMemoryProperties(allocator_, &mem_props); + char line[512]; + size_t off = 0; + for (uint32_t i = 0; i < mem_props->memoryHeapCount && off + 64 < sizeof(line); ++i) { + const bool dl = (mem_props->memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0; + off += std::snprintf(line + off, sizeof(line) - off, "%sheap%u%s use=%lluMB budget=%lluMB", + i ? " | " : "", i, dl ? "(DL)" : "", + (unsigned long long)(budgets[i].usage >> 20), + (unsigned long long)(budgets[i].budget >> 20)); + } + REXLOG_INFO("nrhi-vulkan mem: {} | bufs upload dl={}MB host={}MB, default dl={}MB host={}MB", + line, g_buf_bytes_upload_dl.load(std::memory_order_relaxed) >> 20, + g_buf_bytes_upload_host.load(std::memory_order_relaxed) >> 20, + g_buf_bytes_default_dl.load(std::memory_order_relaxed) >> 20, + g_buf_bytes_default_host.load(std::memory_order_relaxed) >> 20); + } + ring_region_base_ = uint32_t(frame_index_ % kRingRegions) * kRingRegionSize; + ring_region_offset_ = 0; + cmd_.ResetFrameState(); + // Close any emulated render pass and flush the CP's queued barriers: the + // raw commands recorded below (and by the app callback) go into the same + // deferred command buffer and must be outside any render pass. + cp_->SubmitBarriers(true); + EnsureWhiteTexture(); + + NrTextureVulkan*& wrapper = guest_outputs_[guest_output_image]; + if (wrapper != nullptr && + (wrapper->desc.width != width || wrapper->desc.height != height || + wrapper->attachment_view != guest_output_image_view)) { + // Same VkImage handle value, different geometry/view: a recreated + // image reusing the handle. Drop the stale wrapper (image + view are + // presenter-owned) and its framebuffers. + DestroyGuestOutputWrapper(wrapper); + wrapper = nullptr; + } + if (wrapper == nullptr) { + wrapper = new NrTextureVulkan(); + wrapper->image = guest_output_image; + wrapper->desc.width = width; + wrapper->desc.height = height; + wrapper->desc.format = Format::kR10G10B10A2_UNORM; + wrapper->desc.usage = nrhi::kTextureUsageRenderTarget; + wrapper->vk_format = ui::vulkan::VulkanPresenter::kGuestOutputFormat; + wrapper->aspect = VK_IMAGE_ASPECT_COLOR_BIT; + wrapper->attachment_view = guest_output_image_view; + wrapper->attachment_view_owned = false; + wrapper->is_guest_output = true; + // The presenter keeps a small guest-output mailbox; evict wrappers of + // images it has replaced. + if (guest_outputs_.size() > 6) { + for (auto it = guest_outputs_.begin(); it != guest_outputs_.end();) { + if (it->second != wrapper && it->first != guest_output_image) { + DestroyGuestOutputWrapper(it->second); + it = guest_outputs_.erase(it); + } else { + ++it; + } + } + } + } + // Acquire contract: UNDEFINED before the image's first ever write, the + // presenter's internal layout otherwise. + wrapper->current_layout = guest_output_ever_written + ? ui::vulkan::VulkanPresenter::kGuestOutputInternalLayout + : VK_IMAGE_LAYOUT_UNDEFINED; + wrapper->pending_clear = false; + *guest_output_out = wrapper; + return &cmd_; + } + + void EndFrame() { + cmd_.EndFrame(); + // Render-thread CPU attribution for slow frames (throttled 8 per 5 s). + const uint64_t total_us = prof_.Total(); + if (total_us >= 4000) { + const auto now = std::chrono::steady_clock::now(); + static std::chrono::steady_clock::time_point s_window_start{}; + static uint32_t s_window_count = 0; + if (now - s_window_start > std::chrono::seconds(5)) { + s_window_start = now; + s_window_count = 0; + } + if (++s_window_count <= 8) { + REXLOG_INFO( + "nrhi-vulkan: SLOW frame {}us: pass_open={}us/{} flush={}us/{} copies={}us/{} " + "table_miss={}us/{} set0_miss={}us/{} view_destroy={}us/{} view_create={}us/{} " + "const={}us/{} pso={}us/{}", + total_us, prof_.pass_open.us, prof_.pass_open.count, prof_.flush_barriers.us, + prof_.flush_barriers.count, prof_.copies.us, prof_.copies.count, prof_.table_miss.us, + prof_.table_miss.count, prof_.set0_miss.us, prof_.set0_miss.count, + prof_.view_destroy.us, prof_.view_destroy.count, prof_.view_create.us, + prof_.view_create.count, prof_.const_slice.us, prof_.const_slice.count, + prof_.pipeline_build.us, prof_.pipeline_build.count); + } + } + } + + NrFrameProf prof_; + + // --- internals shared with NrCmdVulkan --- + + VulkanCommandProcessor* cp() { return cp_; } + const ui::vulkan::VulkanDevice* vulkan_device() const { return vulkan_device_; } + VkBuffer ring_buffer() const { return ring_buffer_; } + VkImageView white_view() const { return white_view_; } + + // Bump-allocates a 256-aligned slice in the current ring region and copies + // the shadow block in. Returns the dynamic offset. Render thread only. + uint32_t AllocateConstantSlice(const void* data, uint32_t size_bytes) { + if (ring_mapping_ == nullptr) return 0; + NrProfScope prof_scope(prof_.const_slice); + uint32_t aligned = (size_bytes + 255u) & ~255u; + if (ring_region_offset_ + aligned > kRingRegionSize) { + // Region exhausted: wrap to the region start. A visual glitch (draws + // early in the frame may see late constants) beats a crash. + if (!ring_wrap_logged_) { + ring_wrap_logged_ = true; + REXLOG_ERROR( + "nrhi-vulkan: root-constant ring region exhausted ({} bytes/frame), wrapping - " + "constants may glitch this session", + kRingRegionSize); + } + ring_region_offset_ = 0; + } + uint32_t offset = ring_region_base_ + ring_region_offset_; + std::memcpy(ring_mapping_ + offset, data, size_bytes); + ring_region_offset_ += aligned; + return offset; + } + + // Ensures the texture's tracked layout equals the state's layout, pushing + // an internal transition when the app relied on a D3D12 initial state (or + // hasn't flushed a queued barrier - the caller submits right after). + void EnsureLayout(NrTextureVulkan* t, const StateInfo& dst) { + if (t->current_layout == dst.layout) return; + const bool undefined = t->current_layout == VK_IMAGE_LAYOUT_UNDEFINED; + cp_->PushImageMemoryBarrier( + t->image, t->WholeRange(), + undefined ? VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT : VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + dst.stage_mask, undefined ? 0 : VK_ACCESS_MEMORY_WRITE_BIT, dst.access_mask, + t->current_layout, dst.layout); + t->current_layout = dst.layout; + } + + // Attachment (identity-swizzle) view of mip 0, created lazily. + VkImageView GetAttachmentView(NrTextureVulkan* t) { + if (t->attachment_view != VK_NULL_HANDLE) return t->attachment_view; + VkImageViewCreateInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + info.image = t->image; + info.viewType = VK_IMAGE_VIEW_TYPE_2D; + info.format = t->vk_format; + info.subresourceRange.aspectMask = t->aspect; + info.subresourceRange.baseMipLevel = 0; + info.subresourceRange.levelCount = 1; + info.subresourceRange.baseArrayLayer = 0; + info.subresourceRange.layerCount = 1; + if (vulkan_device_->functions().vkCreateImageView(vulkan_device_->device(), &info, nullptr, + &t->attachment_view) != VK_SUCCESS) { + REXLOG_ERROR("nrhi-vulkan: attachment view creation failed"); + t->attachment_view = VK_NULL_HANDLE; + } + t->attachment_view_owned = true; + return t->attachment_view; + } + + VkRenderPass GetRenderPass(VkFormat color_format, VkFormat depth_format, uint32_t samples, + bool color_clear, bool depth_clear) { + RenderPassKey key{color_format, depth_format, samples, color_clear, depth_clear}; + auto it = render_passes_.find(key); + if (it != render_passes_.end()) return it->second; + + VkAttachmentDescription attachments[2] = {}; + VkAttachmentReference color_ref = {}; + VkAttachmentReference depth_ref = {}; + uint32_t attachment_count = 0; + if (color_format != VK_FORMAT_UNDEFINED) { + VkAttachmentDescription& a = attachments[attachment_count]; + a.format = color_format; + a.samples = VkSampleCountFlagBits(samples); + a.loadOp = color_clear ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_LOAD; + a.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + a.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + a.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + a.initialLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + a.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + color_ref.attachment = attachment_count; + color_ref.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + ++attachment_count; + } + if (depth_format != VK_FORMAT_UNDEFINED) { + VkAttachmentDescription& a = attachments[attachment_count]; + a.format = depth_format; + a.samples = VkSampleCountFlagBits(samples); + a.loadOp = depth_clear ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_LOAD; + a.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + a.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + a.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + a.initialLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + a.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + depth_ref.attachment = attachment_count; + depth_ref.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + ++attachment_count; + } + + VkSubpassDescription subpass = {}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + if (color_format != VK_FORMAT_UNDEFINED) { + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &color_ref; + } + if (depth_format != VK_FORMAT_UNDEFINED) { + subpass.pDepthStencilAttachment = &depth_ref; + } + + // Explicit external dependencies ordering attachment access between + // back-to-back passes on the same target (D3D12 needed no barrier for + // consecutive passes in the same resource state). + const VkPipelineStageFlags attachment_stages = + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | + VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; + const VkAccessFlags attachment_access = + VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + VkSubpassDependency dependencies[2] = {}; + dependencies[0].srcSubpass = VK_SUBPASS_EXTERNAL; + dependencies[0].dstSubpass = 0; + dependencies[0].srcStageMask = attachment_stages; + dependencies[0].dstStageMask = attachment_stages; + dependencies[0].srcAccessMask = attachment_access; + dependencies[0].dstAccessMask = attachment_access; + dependencies[1].srcSubpass = 0; + dependencies[1].dstSubpass = VK_SUBPASS_EXTERNAL; + dependencies[1].srcStageMask = attachment_stages; + dependencies[1].dstStageMask = attachment_stages; + dependencies[1].srcAccessMask = attachment_access; + dependencies[1].dstAccessMask = attachment_access; + + VkRenderPassCreateInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; + info.attachmentCount = attachment_count; + info.pAttachments = attachments; + info.subpassCount = 1; + info.pSubpasses = &subpass; + info.dependencyCount = 2; + info.pDependencies = dependencies; + VkRenderPass render_pass = VK_NULL_HANDLE; + if (vulkan_device_->functions().vkCreateRenderPass(vulkan_device_->device(), &info, nullptr, + &render_pass) != VK_SUCCESS) { + REXLOG_ERROR("nrhi-vulkan: render pass creation failed"); + return VK_NULL_HANDLE; + } + render_passes_.emplace(key, render_pass); + return render_pass; + } + + VkFramebuffer GetFramebuffer(VkRenderPass render_pass, VkImageView color_view, + VkImageView depth_view, uint32_t width, uint32_t height) { + // Framebuffers are compatible across load-op variants of the same + // attachment set, so the key omits the render pass. + FramebufferKey key{color_view, depth_view, width, height}; + auto it = framebuffers_.find(key); + if (it != framebuffers_.end()) return it->second; + VkImageView attachments[2]; + uint32_t attachment_count = 0; + if (color_view != VK_NULL_HANDLE) attachments[attachment_count++] = color_view; + if (depth_view != VK_NULL_HANDLE) attachments[attachment_count++] = depth_view; + VkFramebufferCreateInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + info.renderPass = render_pass; + info.attachmentCount = attachment_count; + info.pAttachments = attachments; + info.width = width; + info.height = height; + info.layers = 1; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + if (vulkan_device_->functions().vkCreateFramebuffer(vulkan_device_->device(), &info, nullptr, + &framebuffer) != VK_SUCCESS) { + REXLOG_ERROR("nrhi-vulkan: framebuffer creation failed"); + return VK_NULL_HANDLE; + } + framebuffers_.emplace(key, framebuffer); + return framebuffer; + } + + // Retires all cached framebuffers referencing the view. Render thread. + void RetireFramebuffersForView(VkImageView view) { + if (view == VK_NULL_HANDLE) return; + const uint64_t submission = cp_->GetCurrentSubmission(); + for (auto it = framebuffers_.begin(); it != framebuffers_.end();) { + if (it->first.color_view == view || it->first.depth_view == view) { + std::lock_guard lock(mutex_); + RetiredObject r; + r.submission = submission; + r.framebuffer = it->second; + retired_.push_back(r); + it = framebuffers_.erase(it); + } else { + ++it; + } + } + } + + // Cached set-0 descriptor set for (layout, bound buffer tuple). The ring + // UBO backs the kConstants binding and any never-bound buffer param (so a + // draw before every SetConstantBuffer still has valid descriptors). + VkDescriptorSet GetSet0(NrBindingLayoutVulkan* layout, NrBufferVulkan* const* buffers) { + Set0Key key{}; + key.layout = layout; + key.count = layout->buffer_binding_count; + VkBuffer vk_buffers[nrhi::kMaxBindingParams]; + for (uint32_t i = 0; i < key.count; ++i) { + vk_buffers[i] = buffers[i] != nullptr ? buffers[i]->buffer : ring_buffer_; + key.buffers[i] = vk_buffers[i]; + } + auto it = set0_sets_.find(key); + if (it != set0_sets_.end()) return it->second.set; + + NrProfScope prof_scope(prof_.set0_miss); + SetEntry entry; + if (!AllocateDescriptorSet(layout->set0_layout, &entry)) return VK_NULL_HANDLE; + VkDescriptorBufferInfo buffer_infos[nrhi::kMaxBindingParams]; + VkWriteDescriptorSet writes[nrhi::kMaxBindingParams]; + uint32_t write_count = 0; + for (uint32_t i = 0; i < layout->param_count; ++i) { + const NrBindingLayoutVulkan::ParamInfo& p = layout->params[i]; + if (p.kind == nrhi::BindingParamKind::kTextureTable) continue; + VkDescriptorBufferInfo& bi = buffer_infos[write_count]; + bi.buffer = vk_buffers[p.set0_binding]; + bi.offset = 0; // the bind offset rides the dynamic offset + bi.range = p.descriptor_type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC ? kSsboBindRange + : kUboBindRange; + VkWriteDescriptorSet& w = writes[write_count]; + w = {}; + w.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + w.dstSet = entry.set; + w.dstBinding = p.set0_binding; + w.descriptorCount = 1; + w.descriptorType = p.descriptor_type; + w.pBufferInfo = &bi; + ++write_count; + } + vulkan_device_->functions().vkUpdateDescriptorSets(vulkan_device_->device(), write_count, + writes, 0, nullptr); + set0_sets_.emplace(key, entry); + return entry.set; + } + + // Cached texture-table set for (per-param set layout, ordered view tuple) + // with white-fallback substitution already applied by the caller. + VkDescriptorSet GetTableSet(VkDescriptorSetLayout set_layout, + NrTextureViewVulkan* const* views, uint32_t count) { + TableKey key{}; + key.layout = set_layout; + key.count = count; + for (uint32_t i = 0; i < count; ++i) { + key.views[i] = views[i]; + } + auto it = table_sets_.find(key); + if (it != table_sets_.end()) return it->second.set; + + NrProfScope prof_scope(prof_.table_miss); + SetEntry entry; + if (!AllocateDescriptorSet(set_layout, &entry)) return VK_NULL_HANDLE; + VkDescriptorImageInfo image_infos[nrhi::kMaxTextureTableSize]; + VkWriteDescriptorSet writes[nrhi::kMaxTextureTableSize]; + for (uint32_t i = 0; i < count; ++i) { + image_infos[i] = {}; + image_infos[i].imageView = views[i] != nullptr ? views[i]->view : white_view_; + image_infos[i].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + writes[i] = {}; + writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[i].dstSet = entry.set; + writes[i].dstBinding = i; + writes[i].descriptorCount = 1; + writes[i].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + writes[i].pImageInfo = &image_infos[i]; + } + vulkan_device_->functions().vkUpdateDescriptorSets(vulkan_device_->device(), count, writes, 0, + nullptr); + table_sets_.emplace(key, entry); + return entry.set; + } + + // Both topology variants are built eagerly at creation (the scene destroys + // its shader modules right after CreateGraphicsPipeline, so nothing may be + // built lazily from them at draw time). + VkPipeline GetPipelineVariant(NrPipelineVulkan* p, nrhi::PrimitiveTopology topology) { + return topology == nrhi::PrimitiveTopology::kTriangleList ? p->list_pipeline + : p->strip_pipeline; + } + + std::vector& pending_clear_textures() { return pending_clear_textures_; } + + private: + struct RetiredObject { + uint64_t submission = 0; + VkBuffer buffer = VK_NULL_HANDLE; + VmaAllocation buffer_allocation = VK_NULL_HANDLE; + VkImage image = VK_NULL_HANDLE; + VmaAllocation image_allocation = VK_NULL_HANDLE; + VkImageView view = VK_NULL_HANDLE; + VkPipeline pipelines[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE}; + VkShaderModule shader_module = VK_NULL_HANDLE; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + VkDescriptorSet descriptor_set = VK_NULL_HANDLE; + VkDescriptorPool descriptor_pool = VK_NULL_HANDLE; + }; + + struct SetEntry { + VkDescriptorSet set = VK_NULL_HANDLE; + VkDescriptorPool pool = VK_NULL_HANDLE; + }; + + struct RenderPassKey { + VkFormat color_format; + VkFormat depth_format; + uint32_t samples; + bool color_clear; + bool depth_clear; + bool operator<(const RenderPassKey& o) const { + if (color_format != o.color_format) return color_format < o.color_format; + if (depth_format != o.depth_format) return depth_format < o.depth_format; + if (samples != o.samples) return samples < o.samples; + if (color_clear != o.color_clear) return color_clear < o.color_clear; + return depth_clear < o.depth_clear; + } + }; + + struct FramebufferKey { + VkImageView color_view; + VkImageView depth_view; + uint32_t width; + uint32_t height; + bool operator<(const FramebufferKey& o) const { + if (color_view != o.color_view) return color_view < o.color_view; + if (depth_view != o.depth_view) return depth_view < o.depth_view; + if (width != o.width) return width < o.width; + return height < o.height; + } + }; + + struct Set0Key { + const NrBindingLayoutVulkan* layout; + VkBuffer buffers[nrhi::kMaxBindingParams]; + uint32_t count; + bool operator<(const Set0Key& o) const { + if (layout != o.layout) return layout < o.layout; + if (count != o.count) return count < o.count; + for (uint32_t i = 0; i < count; ++i) { + if (buffers[i] != o.buffers[i]) return buffers[i] < o.buffers[i]; + } + return false; + } + }; + + struct TableKey { + VkDescriptorSetLayout layout; + NrTextureViewVulkan* views[nrhi::kMaxTextureTableSize]; + uint32_t count; + bool operator<(const TableKey& o) const { + if (layout != o.layout) return layout < o.layout; + if (count != o.count) return count < o.count; + for (uint32_t i = 0; i < count; ++i) { + if (views[i] != o.views[i]) return views[i] < o.views[i]; + } + return false; + } + }; + + bool AllocateDescriptorSet(VkDescriptorSetLayout layout, SetEntry* out) { + const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device_->functions(); + const VkDevice device = vulkan_device_->device(); + for (int attempt = 0; attempt < 2; ++attempt) { + if (!descriptor_pools_.empty()) { + VkDescriptorSetAllocateInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + info.descriptorPool = descriptor_pools_.back(); + info.descriptorSetCount = 1; + info.pSetLayouts = &layout; + VkDescriptorSet set = VK_NULL_HANDLE; + if (dfn.vkAllocateDescriptorSets(device, &info, &set) == VK_SUCCESS) { + out->set = set; + out->pool = descriptor_pools_.back(); + return true; + } + } + // Grow: sets are freed individually on retirement, so pools need the + // free-descriptor-set flag. Immutable-sampler bindings still consume + // SAMPLER pool capacity. + VkDescriptorPoolSize sizes[4] = { + {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 4096}, + {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, 1024}, + {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 16384}, + {VK_DESCRIPTOR_TYPE_SAMPLER, 2048}, + }; + VkDescriptorPoolCreateInfo pool_info = {}; + pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + pool_info.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; + pool_info.maxSets = 4096; + pool_info.poolSizeCount = 4; + pool_info.pPoolSizes = sizes; + VkDescriptorPool pool = VK_NULL_HANDLE; + if (dfn.vkCreateDescriptorPool(device, &pool_info, nullptr, &pool) != VK_SUCCESS) { + REXLOG_ERROR("nrhi-vulkan: descriptor pool creation failed"); + return false; + } + descriptor_pools_.push_back(pool); + } + REXLOG_ERROR("nrhi-vulkan: descriptor set allocation failed"); + return false; + } + + void RetireDescriptorSetLocked(const SetEntry& entry, uint64_t submission) { + RetiredObject r; + r.submission = submission; + r.descriptor_set = entry.set; + r.descriptor_pool = entry.pool; + retired_.push_back(r); + } + + void DrainRetired(uint64_t completed) { + const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device_->functions(); + const VkDevice device = vulkan_device_->device(); + std::erase_if(retired_, [&](const RetiredObject& r) { + if (r.submission >= completed) return false; + if (r.framebuffer != VK_NULL_HANDLE) dfn.vkDestroyFramebuffer(device, r.framebuffer, nullptr); + if (r.view != VK_NULL_HANDLE) dfn.vkDestroyImageView(device, r.view, nullptr); + for (VkPipeline pipeline : r.pipelines) { + if (pipeline != VK_NULL_HANDLE) dfn.vkDestroyPipeline(device, pipeline, nullptr); + } + if (r.shader_module != VK_NULL_HANDLE) { + dfn.vkDestroyShaderModule(device, r.shader_module, nullptr); + } + if (r.descriptor_set != VK_NULL_HANDLE) { + dfn.vkFreeDescriptorSets(device, r.descriptor_pool, 1, &r.descriptor_set); + } + if (r.image != VK_NULL_HANDLE) vmaDestroyImage(allocator_, r.image, r.image_allocation); + if (r.buffer != VK_NULL_HANDLE) vmaDestroyBuffer(allocator_, r.buffer, r.buffer_allocation); + return true; + }); + } + + void DestroyGuestOutputWrapper(NrTextureVulkan* wrapper) { + RetireFramebuffersForView(wrapper->attachment_view); + if (cmd_.rt_color_ == wrapper) cmd_.rt_color_ = nullptr; + std::erase(pending_clear_textures_, wrapper); + delete wrapper; // image + view are presenter-owned + } + + // 1x1 white RGBA8 fallback for unbound texture-table tail entries. The + // upload is recorded into the deferred command buffer on the first + // BeginFrame (a submission is guaranteed open there), through the CP + // barrier queue - the simplest arrangement that needs no extra queue + // submission machinery. + void EnsureWhiteTexture() { + if (white_view_ != VK_NULL_HANDLE) return; + VkImageCreateInfo image_info = {}; + image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + image_info.imageType = VK_IMAGE_TYPE_2D; + image_info.format = VK_FORMAT_R8G8B8A8_UNORM; + image_info.extent = {1, 1, 1}; + image_info.mipLevels = 1; + image_info.arrayLayers = 1; + image_info.samples = VK_SAMPLE_COUNT_1_BIT; + image_info.tiling = VK_IMAGE_TILING_OPTIMAL; + image_info.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + VmaAllocationCreateInfo alloc_info = {}; + alloc_info.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE; + if (vmaCreateImage(allocator_, &image_info, &alloc_info, &white_image_, &white_allocation_, + nullptr) != VK_SUCCESS) { + REXLOG_ERROR("nrhi-vulkan: white fallback image creation failed"); + return; + } + + VkBufferCreateInfo staging_info = {}; + staging_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + staging_info.size = 4; + staging_info.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; + staging_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + VmaAllocationCreateInfo staging_alloc_info = {}; + staging_alloc_info.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT | + VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT; + staging_alloc_info.usage = VMA_MEMORY_USAGE_AUTO; + staging_alloc_info.requiredFlags = + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + VmaAllocationInfo staging_result = {}; + if (vmaCreateBuffer(allocator_, &staging_info, &staging_alloc_info, &white_staging_, + &white_staging_allocation_, &staging_result) != VK_SUCCESS) { + REXLOG_ERROR("nrhi-vulkan: white fallback staging buffer creation failed"); + return; + } + const uint32_t white = 0xFFFFFFFFu; + std::memcpy(staging_result.pMappedData, &white, sizeof(white)); + + VkImageSubresourceRange range = ui::vulkan::util::InitializeSubresourceRange(); + cp_->PushImageMemoryBarrier(white_image_, range, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + cp_->SubmitBarriers(true); + VkBufferImageCopy region = {}; + region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + region.imageSubresource.layerCount = 1; + region.imageExtent = {1, 1, 1}; + cp_->deferred_command_buffer().CmdVkCopyBufferToImage( + white_staging_, white_image_, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + cp_->PushImageMemoryBarrier(white_image_, range, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + cp_->SubmitBarriers(true); + + VkImageViewCreateInfo view_info = {}; + view_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + view_info.image = white_image_; + view_info.viewType = VK_IMAGE_VIEW_TYPE_2D; + view_info.format = VK_FORMAT_R8G8B8A8_UNORM; + view_info.subresourceRange = range; + if (vulkan_device_->functions().vkCreateImageView(vulkan_device_->device(), &view_info, + nullptr, &white_view_) != VK_SUCCESS) { + REXLOG_ERROR("nrhi-vulkan: white fallback view creation failed"); + white_view_ = VK_NULL_HANDLE; + } + } + + VkPipeline BuildPipeline(const NrPipelineVulkan& p, VkPrimitiveTopology topology) { + NrProfScope prof_scope(prof_.pipeline_build); + const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device_->functions(); + const ui::vulkan::VulkanDevice::Properties& props = vulkan_device_->properties(); + + VkPipelineShaderStageCreateInfo stages[2] = {}; + stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; + stages[0].module = p.vs_module; + stages[0].pName = p.vs_entry.c_str(); + stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; + stages[1].module = p.ps_module; + stages[1].pName = p.ps_entry.c_str(); + + VkVertexInputBindingDescription binding = {}; + VkVertexInputAttributeDescription attributes[16] = {}; + VkPipelineVertexInputStateCreateInfo vertex_input = {}; + vertex_input.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; + if (!p.input_elements.empty() && p.input_elements.size() <= 16) { + binding.binding = 0; + binding.stride = p.vertex_stride; + binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + for (size_t i = 0; i < p.input_elements.size(); ++i) { + const nrhi::InputElementDesc& e = p.input_elements[i]; + attributes[i].location = e.location; + attributes[i].binding = 0; + attributes[i].format = ToVkFormat(e.format); + attributes[i].offset = e.byte_offset; + } + vertex_input.vertexBindingDescriptionCount = 1; + vertex_input.pVertexBindingDescriptions = &binding; + vertex_input.vertexAttributeDescriptionCount = uint32_t(p.input_elements.size()); + vertex_input.pVertexAttributeDescriptions = attributes; + } + + VkPipelineInputAssemblyStateCreateInfo input_assembly = {}; + input_assembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; + input_assembly.topology = topology; + + VkPipelineViewportStateCreateInfo viewport_state = {}; + viewport_state.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; + viewport_state.viewportCount = 1; + viewport_state.scissorCount = 1; + + VkPipelineRasterizationStateCreateInfo raster = {}; + raster.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; + // depthClampEnable requires the depthClamp feature; the SDK device + // enables it when supported (Properties::depthClamp). If unsupported, + // fall back to clipping and note it once. + if (!p.depth_clip) { + if (props.depthClamp) { + raster.depthClampEnable = VK_TRUE; + } else { + static bool depth_clamp_unsupported_logged = false; + if (!depth_clamp_unsupported_logged) { + depth_clamp_unsupported_logged = true; + REXLOG_ERROR( + "nrhi-vulkan: pipeline wants depth clamp (depth_clip=false) but the device has no " + "depthClamp feature; using depth clipping"); + } + } + } + raster.polygonMode = VK_POLYGON_MODE_FILL; + raster.cullMode = p.cull == nrhi::CullMode::kFront ? VK_CULL_MODE_FRONT_BIT + : p.cull == nrhi::CullMode::kBack ? VK_CULL_MODE_BACK_BIT + : VK_CULL_MODE_NONE; + // Matches D3D FrontCounterClockwise=FALSE under the negative-viewport + // y-flip: the geometry->pixel mapping equals D3D, so the screen-space + // winding is unchanged. + raster.frontFace = VK_FRONT_FACE_CLOCKWISE; + raster.lineWidth = 1.0f; + + VkPipelineMultisampleStateCreateInfo multisample = {}; + multisample.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; + multisample.rasterizationSamples = VkSampleCountFlagBits(p.sample_count); + + VkPipelineDepthStencilStateCreateInfo depth_stencil = {}; + depth_stencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; + depth_stencil.depthTestEnable = p.depth.test_enable; + depth_stencil.depthWriteEnable = p.depth.write_enable; + depth_stencil.depthCompareOp = ToVkCompare(p.depth.func); + + VkPipelineColorBlendAttachmentState blend_attachment = {}; + blend_attachment.blendEnable = p.blend.enable; + blend_attachment.srcColorBlendFactor = ToVkBlend(p.blend.src); + blend_attachment.dstColorBlendFactor = ToVkBlend(p.blend.dst); + blend_attachment.colorBlendOp = ToVkBlendOp(p.blend.op); + blend_attachment.srcAlphaBlendFactor = ToVkBlend(p.blend.src_alpha); + blend_attachment.dstAlphaBlendFactor = ToVkBlend(p.blend.dst_alpha); + blend_attachment.alphaBlendOp = ToVkBlendOp(p.blend.op_alpha); + blend_attachment.colorWriteMask = p.blend.write_mask; // RGBA bits match VkColorComponentFlags + VkPipelineColorBlendStateCreateInfo blend = {}; + blend.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; + if (p.rtv_format != Format::kUnknown) { + blend.attachmentCount = 1; + blend.pAttachments = &blend_attachment; + } + + const VkDynamicState dynamic_states[2] = {VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR}; + VkPipelineDynamicStateCreateInfo dynamic = {}; + dynamic.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; + dynamic.dynamicStateCount = 2; + dynamic.pDynamicStates = dynamic_states; + + VkRenderPass render_pass = GetRenderPass( + p.rtv_format != Format::kUnknown ? ToVkFormat(p.rtv_format) : VK_FORMAT_UNDEFINED, + p.dsv_format != Format::kUnknown ? ToVkFormat(p.dsv_format) : VK_FORMAT_UNDEFINED, + p.sample_count, false, false); + if (render_pass == VK_NULL_HANDLE) return VK_NULL_HANDLE; + + VkGraphicsPipelineCreateInfo info = {}; + info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; + info.stageCount = 2; + info.pStages = stages; + info.pVertexInputState = &vertex_input; + info.pInputAssemblyState = &input_assembly; + info.pViewportState = &viewport_state; + info.pRasterizationState = &raster; + info.pMultisampleState = &multisample; + info.pDepthStencilState = &depth_stencil; + info.pColorBlendState = &blend; + info.pDynamicState = &dynamic; + info.layout = p.pipeline_layout; + info.renderPass = render_pass; + info.subpass = 0; + VkPipeline pipeline = VK_NULL_HANDLE; + VkResult result = dfn.vkCreateGraphicsPipelines(vulkan_device_->device(), VK_NULL_HANDLE, 1, + &info, nullptr, &pipeline); + if (result != VK_SUCCESS) { + REXLOG_ERROR("nrhi-vulkan: vkCreateGraphicsPipelines failed ({}, vs '{}' ps '{}')", + int32_t(result), p.vs_entry, p.ps_entry); + return VK_NULL_HANDLE; + } + return pipeline; + } + + friend class NrCmdVulkan; + + VulkanCommandProcessor* cp_; + const ui::vulkan::VulkanDevice* vulkan_device_; + NrCmdVulkan cmd_; + + VmaAllocator allocator_ = VK_NULL_HANDLE; + + // Root-constant ring UBO. + VkBuffer ring_buffer_ = VK_NULL_HANDLE; + VmaAllocation ring_allocation_ = VK_NULL_HANDLE; + uint8_t* ring_mapping_ = nullptr; + uint64_t frame_index_ = 0; + uint32_t ring_region_base_ = 0; + uint32_t ring_region_offset_ = 0; + bool ring_wrap_logged_ = false; + + // White 1x1 fallback. + VkImage white_image_ = VK_NULL_HANDLE; + VmaAllocation white_allocation_ = VK_NULL_HANDLE; + VkImageView white_view_ = VK_NULL_HANDLE; + VkBuffer white_staging_ = VK_NULL_HANDLE; + VmaAllocation white_staging_allocation_ = VK_NULL_HANDLE; + + // mutex_ guards retired_ and the live_* sets (creation and deferred + // destruction are thread-safe); the caches below are render-thread-only. + std::mutex mutex_; + std::vector retired_; + // Every app-created object still alive (inserted at Create*, erased at + // DestroyDeferred). The app holds its caches (meshes, texture store, + // rings, PSOs) across the whole session and has no device-teardown + // notification, so whatever is still here at device destruction is + // released by the destructor sweep; child objects must not outlive + // vkDestroyDevice (VUID-vkDestroyDevice-device-05137). + std::unordered_set live_buffers_; + std::unordered_set live_textures_; + std::unordered_set live_views_; + std::unordered_set live_pipelines_; + std::unordered_set live_shaders_; + + std::map render_passes_; + std::map framebuffers_; + std::vector descriptor_pools_; + std::map set0_sets_; + std::map table_sets_; + std::vector layouts_; // owned; no destroy API on the interface + std::map guest_outputs_; + std::vector pending_clear_textures_; +}; + +// --------------------------------------------------------------------------- +// NrCmdVulkan implementation. +// --------------------------------------------------------------------------- + +void NrCmdVulkan::ResetFrameState() { + layout_ = nullptr; + pipeline_ = nullptr; + bound_pipeline_ = VK_NULL_HANDLE; + topology_ = nrhi::PrimitiveTopology::kTriangleList; + std::memset(constants_shadow_, 0, sizeof(constants_shadow_)); + constants_dirty_ = false; + constants_ring_offset_ = ~0u; + std::memset(set0_buffers_, 0, sizeof(set0_buffers_)); + std::memset(set0_offsets_, 0, sizeof(set0_offsets_)); + set0_rebind_needed_ = true; + set0_bound_ = VK_NULL_HANDLE; + std::memset(table_views_, 0, sizeof(table_views_)); + std::memset(table_dirty_, 0, sizeof(table_dirty_)); + rt_color_ = nullptr; + rt_depth_ = nullptr; + if (render_pass_open_) { + // Should have been closed by the previous EndFrame - close in the + // current command stream is impossible (different submission), so just + // drop the flag. + REXLOG_ERROR("nrhi-vulkan: render pass leaked across frames (state dropped)"); + render_pass_open_ = false; + } +} + +void NrCmdVulkan::EndFrame() { + EndRenderPassIfOpen(); + // Flush pending clears no draw consumed (D3D12 clears were immediate - + // preserve the observable "cleared even without a subsequent draw" + // behavior). + auto& pending = device->pending_clear_textures(); + while (!pending.empty()) { + NrTextureVulkan* t = pending.back(); + if (!t->pending_clear) { + pending.pop_back(); + continue; + } + // FlushPendingClear via Barrier-style path: an empty CLEAR pass. + const bool is_depth = t->aspect == VK_IMAGE_ASPECT_DEPTH_BIT; + device->EnsureLayout(t, ToStateInfo(is_depth ? ResourceState::kDepthWrite + : ResourceState::kRenderTarget)); + device->cp()->SubmitBarriers(true); + VkRenderPass render_pass = device->GetRenderPass( + is_depth ? VK_FORMAT_UNDEFINED : t->vk_format, is_depth ? t->vk_format : VK_FORMAT_UNDEFINED, + t->desc.sample_count, !is_depth, is_depth); + VkImageView view = device->GetAttachmentView(t); + if (render_pass != VK_NULL_HANDLE && view != VK_NULL_HANDLE) { + VkFramebuffer framebuffer = + device->GetFramebuffer(render_pass, is_depth ? VK_NULL_HANDLE : view, + is_depth ? view : VK_NULL_HANDLE, t->desc.width, t->desc.height); + if (framebuffer != VK_NULL_HANDLE) { + VkClearValue clear_value; + if (is_depth) { + clear_value.depthStencil = {t->clear_depth, 0}; + } else { + std::memcpy(clear_value.color.float32, t->clear_color, sizeof(t->clear_color)); + } + VkRenderPassBeginInfo begin_info = {}; + begin_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + begin_info.renderPass = render_pass; + begin_info.framebuffer = framebuffer; + begin_info.renderArea.extent = {t->desc.width, t->desc.height}; + begin_info.clearValueCount = 1; + begin_info.pClearValues = &clear_value; + DeferredCommandBuffer& cmd = device->cp()->deferred_command_buffer(); + cmd.CmdVkBeginRenderPass(&begin_info, VK_SUBPASS_CONTENTS_INLINE); + cmd.CmdVkEndRenderPass(); + } + } + t->pending_clear = false; + pending.pop_back(); + } + // Submit remaining queued barriers (including the app's release of the + // guest output back to kGuestOutput, if it did not flush explicitly). + device->cp()->SubmitBarriers(true); +} + +void NrCmdVulkan::EndRenderPassIfOpen() { + if (!render_pass_open_) return; + device->cp()->deferred_command_buffer().CmdVkEndRenderPass(); + render_pass_open_ = false; +} + +bool NrCmdVulkan::EnsureRenderPassOpen() { + if (render_pass_open_) return true; + NrProfScope prof_scope(device->prof_.pass_open); + if (rt_color_ == nullptr && rt_depth_ == nullptr) { + static bool no_targets_logged = false; + if (!no_targets_logged) { + no_targets_logged = true; + REXLOG_ERROR("nrhi-vulkan: draw with no render targets latched"); + } + return false; + } + + // Bring attachments to attachment layout (covers D3D12 initial-state + // reliance) and record every queued barrier before the pass begins. + if (rt_color_ != nullptr) { + device->EnsureLayout(rt_color_, ToStateInfo(ResourceState::kRenderTarget)); + } + if (rt_depth_ != nullptr) { + device->EnsureLayout(rt_depth_, ToStateInfo(ResourceState::kDepthWrite)); + } + device->cp()->SubmitBarriers(true); + + const bool color_clear = rt_color_ != nullptr && rt_color_->pending_clear; + const bool depth_clear = rt_depth_ != nullptr && rt_depth_->pending_clear; + const uint32_t samples = rt_color_ != nullptr ? rt_color_->desc.sample_count + : rt_depth_->desc.sample_count; + VkRenderPass render_pass = device->GetRenderPass( + rt_color_ != nullptr ? rt_color_->vk_format : VK_FORMAT_UNDEFINED, + rt_depth_ != nullptr ? rt_depth_->vk_format : VK_FORMAT_UNDEFINED, samples, color_clear, + depth_clear); + if (render_pass == VK_NULL_HANDLE) return false; + VkImageView color_view = + rt_color_ != nullptr ? device->GetAttachmentView(rt_color_) : VK_NULL_HANDLE; + VkImageView depth_view = + rt_depth_ != nullptr ? device->GetAttachmentView(rt_depth_) : VK_NULL_HANDLE; + const uint32_t width = rt_color_ != nullptr ? rt_color_->desc.width : rt_depth_->desc.width; + const uint32_t height = rt_color_ != nullptr ? rt_color_->desc.height : rt_depth_->desc.height; + VkFramebuffer framebuffer = + device->GetFramebuffer(render_pass, color_view, depth_view, width, height); + if (framebuffer == VK_NULL_HANDLE) return false; + + VkClearValue clear_values[2]; + uint32_t clear_value_count = 0; + if (rt_color_ != nullptr) { + std::memcpy(clear_values[clear_value_count].color.float32, rt_color_->clear_color, + sizeof(rt_color_->clear_color)); + ++clear_value_count; + } + if (rt_depth_ != nullptr) { + clear_values[clear_value_count].depthStencil = {rt_depth_->clear_depth, 0}; + ++clear_value_count; + } + if (color_clear) { + rt_color_->pending_clear = false; + std::erase(device->pending_clear_textures(), rt_color_); + } + if (depth_clear) { + rt_depth_->pending_clear = false; + std::erase(device->pending_clear_textures(), rt_depth_); + } + + // Draws require the render area to be the full target extent. + VkRenderPassBeginInfo begin_info = {}; + begin_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + begin_info.renderPass = render_pass; + begin_info.framebuffer = framebuffer; + begin_info.renderArea.extent = {width, height}; + begin_info.clearValueCount = clear_value_count; + begin_info.pClearValues = clear_values; + device->cp()->deferred_command_buffer().CmdVkBeginRenderPass(&begin_info, + VK_SUBPASS_CONTENTS_INLINE); + render_pass_open_ = true; + return true; +} + +bool NrCmdVulkan::EnsureDrawState() { + if (layout_ == nullptr || pipeline_ == nullptr) return false; + if (!EnsureRenderPassOpen()) return false; + DeferredCommandBuffer& cmd = device->cp()->deferred_command_buffer(); + + VkPipeline variant = device->GetPipelineVariant(pipeline_, topology_); + if (variant != bound_pipeline_) { + cmd.CmdVkBindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, variant); + bound_pipeline_ = variant; + } + + // Root constants: copy the shadow block into a fresh ring slice when + // dirty; the slice offset rides the kConstants dynamic offset. + if (layout_->constants_param >= 0 && + (constants_dirty_ || constants_ring_offset_ == ~0u)) { + constants_ring_offset_ = device->AllocateConstantSlice( + constants_shadow_, std::max(layout_->constants_size_bytes, 4u)); + constants_dirty_ = false; + set0_rebind_needed_ = true; + } + + if (set0_rebind_needed_) { + // Bound even with zero buffer params - the immutable samplers live in + // set 0 too. + VkDescriptorSet set0 = device->GetSet0(layout_, set0_buffers_); + if (set0 != VK_NULL_HANDLE) { + uint32_t dynamic_offsets[nrhi::kMaxBindingParams]; + for (uint32_t i = 0; i < layout_->param_count; ++i) { + const NrBindingLayoutVulkan::ParamInfo& p = layout_->params[i]; + if (p.kind == nrhi::BindingParamKind::kTextureTable) continue; + dynamic_offsets[p.set0_binding] = + p.kind == nrhi::BindingParamKind::kConstants + ? (constants_ring_offset_ != ~0u ? constants_ring_offset_ : 0) + : set0_offsets_[p.set0_binding]; + } + cmd.CmdVkBindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS, layout_->pipeline_layout, 0, 1, + &set0, layout_->buffer_binding_count, dynamic_offsets); + set0_bound_ = set0; + set0_rebind_needed_ = false; + } + } + + for (uint32_t i = 0; i < layout_->param_count; ++i) { + const NrBindingLayoutVulkan::ParamInfo& p = layout_->params[i]; + if (p.kind != nrhi::BindingParamKind::kTextureTable) continue; + if (!table_dirty_[p.table_index]) continue; + VkDescriptorSet set = device->GetTableSet(layout_->table_layouts[p.table_index], + table_views_[p.table_index], p.table_size); + if (set != VK_NULL_HANDLE) { + cmd.CmdVkBindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS, layout_->pipeline_layout, + p.set_index, 1, &set, 0, nullptr); + table_dirty_[p.table_index] = false; + } + } + return true; +} + +void NrCmdVulkan::SetBindingLayout(nrhi::BindingLayout* layout) { + layout_ = static_cast(layout); + // D3D12 root-signature semantics: all root/binding state resets. + std::memset(constants_shadow_, 0, sizeof(constants_shadow_)); + constants_dirty_ = true; + constants_ring_offset_ = ~0u; + std::memset(set0_buffers_, 0, sizeof(set0_buffers_)); + std::memset(set0_offsets_, 0, sizeof(set0_offsets_)); + set0_rebind_needed_ = true; + set0_bound_ = VK_NULL_HANDLE; + std::memset(table_views_, 0, sizeof(table_views_)); + if (layout_ != nullptr) { + for (uint32_t i = 0; i < layout_->table_count; ++i) { + table_dirty_[i] = true; + } + } +} + +void NrCmdVulkan::SetPipeline(nrhi::Pipeline* pipeline) { + pipeline_ = static_cast(pipeline); +} + +void NrCmdVulkan::SetRootConstants(uint32_t param, uint32_t count, const void* values, + uint32_t dest_offset_in_values) { + (void)param; + if (dest_offset_in_values + count > 64) return; + std::memcpy(constants_shadow_ + dest_offset_in_values, values, count * sizeof(uint32_t)); + constants_dirty_ = true; +} + +void NrCmdVulkan::SetConstantBuffer(uint32_t param, nrhi::Buffer* buffer, uint64_t offset) { + if (layout_ == nullptr || param >= layout_->param_count) return; + const NrBindingLayoutVulkan::ParamInfo& p = layout_->params[param]; + set0_buffers_[p.set0_binding] = static_cast(buffer); + set0_offsets_[p.set0_binding] = uint32_t(offset); + set0_rebind_needed_ = true; +} + +void NrCmdVulkan::SetBufferSrv(uint32_t param, nrhi::Buffer* buffer, uint64_t offset) { + SetConstantBuffer(param, buffer, offset); +} + +void NrCmdVulkan::SetTexture(uint32_t param, nrhi::TextureView* view) { + SetTextures(param, &view, 1); +} + +void NrCmdVulkan::SetTexturePair(uint32_t param, nrhi::TextureView* first, + nrhi::TextureView* second) { + nrhi::TextureView* views[2] = {first, second}; + SetTextures(param, views, 2); +} + +void NrCmdVulkan::SetTextures(uint32_t param, nrhi::TextureView* const* views, uint32_t count) { + if (layout_ == nullptr || param >= layout_->param_count) return; + const NrBindingLayoutVulkan::ParamInfo& p = layout_->params[param]; + if (p.kind != nrhi::BindingParamKind::kTextureTable) return; + for (uint32_t i = 0; i < p.table_size; ++i) { + // Missing tail entries (table declares N, bound M(views[i]) : nullptr; + } + table_dirty_[p.table_index] = true; +} + +void NrCmdVulkan::SetRenderTargets(nrhi::Texture* color, nrhi::Texture* depth) { + EndRenderPassIfOpen(); + rt_color_ = static_cast(color); + rt_depth_ = static_cast(depth); +} + +void NrCmdVulkan::LatchPendingClear(NrTextureVulkan* t, const float* color4, float depth) { + if (color4 != nullptr) { + std::memcpy(t->clear_color, color4, sizeof(t->clear_color)); + } else { + t->clear_depth = depth; + } + if (!t->pending_clear) { + t->pending_clear = true; + device->pending_clear_textures().push_back(t); + } +} + +void NrCmdVulkan::ClearRenderTarget(nrhi::Texture* color, const float color4[4]) { + auto* t = static_cast(color); + if (render_pass_open_ && t == rt_color_) { + VkClearAttachment attachment = {}; + attachment.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + attachment.colorAttachment = 0; + std::memcpy(attachment.clearValue.color.float32, color4, sizeof(float) * 4); + VkClearRect rect = {}; + rect.rect.extent = {t->desc.width, t->desc.height}; + rect.layerCount = 1; + device->cp()->deferred_command_buffer().CmdVkClearAttachments(1, &attachment, 1, &rect); + return; + } + LatchPendingClear(t, color4, 0.0f); +} + +void NrCmdVulkan::ClearDepth(nrhi::Texture* depth, float value) { + auto* t = static_cast(depth); + if (render_pass_open_ && t == rt_depth_) { + VkClearAttachment attachment = {}; + attachment.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + attachment.clearValue.depthStencil = {value, 0}; + VkClearRect rect = {}; + rect.rect.extent = {t->desc.width, t->desc.height}; + rect.layerCount = 1; + device->cp()->deferred_command_buffer().CmdVkClearAttachments(1, &attachment, 1, &rect); + return; + } + LatchPendingClear(t, nullptr, value); +} + +void NrCmdVulkan::SetViewport(const nrhi::Viewport& viewport) { + // D3D top-left origin -> negative-height Vulkan viewport (core 1.1 + // maintenance1 behavior): unchanged shaders, unchanged pixel math. + VkViewport vk_viewport; + vk_viewport.x = viewport.x; + vk_viewport.y = viewport.y + viewport.height; + vk_viewport.width = viewport.width; + vk_viewport.height = -viewport.height; + vk_viewport.minDepth = viewport.min_depth; + vk_viewport.maxDepth = viewport.max_depth; + device->cp()->deferred_command_buffer().CmdVkSetViewport(0, 1, &vk_viewport); +} + +void NrCmdVulkan::SetScissor(const nrhi::Rect& rect) { + VkRect2D scissor; + scissor.offset.x = rect.left; + scissor.offset.y = rect.top; + scissor.extent.width = uint32_t(std::max(rect.right - rect.left, 0)); + scissor.extent.height = uint32_t(std::max(rect.bottom - rect.top, 0)); + device->cp()->deferred_command_buffer().CmdVkSetScissor(0, 1, &scissor); +} + +void NrCmdVulkan::SetVertexBuffer(nrhi::Buffer* buffer, uint64_t offset, uint32_t size_bytes, + uint32_t stride) { + (void)size_bytes; // the pipeline's vertex stride governs fetch; Vulkan + // binds (buffer, offset) only + (void)stride; + VkBuffer vk_buffer = static_cast(buffer)->buffer; + VkDeviceSize vk_offset = offset; + device->cp()->deferred_command_buffer().CmdVkBindVertexBuffers(0, 1, &vk_buffer, &vk_offset); +} + +void NrCmdVulkan::SetIndexBuffer(nrhi::Buffer* buffer, uint64_t offset, uint32_t size_bytes) { + (void)size_bytes; + device->cp()->deferred_command_buffer().CmdVkBindIndexBuffer( + static_cast(buffer)->buffer, offset, VK_INDEX_TYPE_UINT16); +} + +void NrCmdVulkan::SetPrimitiveTopology(nrhi::PrimitiveTopology topology) { + topology_ = topology; +} + +void NrCmdVulkan::Draw(uint32_t vertex_count, uint32_t start_vertex) { + if (!EnsureDrawState()) return; + device->cp()->deferred_command_buffer().CmdVkDraw(vertex_count, 1, start_vertex, 0); +} + +void NrCmdVulkan::DrawIndexed(uint32_t index_count, uint32_t start_index, int32_t base_vertex) { + if (!EnsureDrawState()) return; + device->cp()->deferred_command_buffer().CmdVkDrawIndexed(index_count, 1, start_index, + base_vertex, 0); +} + +void NrCmdVulkan::CopyBufferToTexture(nrhi::Texture* dst, uint32_t mip, uint32_t array_slice, + nrhi::Buffer* src, uint64_t src_offset, uint32_t row_pitch, + uint32_t width, uint32_t height, uint32_t depth) { + NrProfScope prof_scope(device->prof_.copies); + auto* t = static_cast(dst); + auto* b = static_cast(src); + EndRenderPassIfOpen(); + // Cover the D3D12 initial-state reliance (created-in-COPY_DEST textures + // are copied into without an explicit barrier), then record every queued + // barrier so the copy is ordered after them. + device->EnsureLayout(t, ToStateInfo(ResourceState::kCopyDest)); + device->cp()->SubmitBarriers(true); + + const uint32_t block_width = nrhi::FormatBlockWidth(t->desc.format); + const uint32_t bytes_per_block = FormatBytesPerBlock(t->desc.format); + // The given width/height are the D3D12 block-aligned footprint dims; + // Vulkan wants the actual (unaligned) extent for small BC mips - clamp + // against the mip chain dims from the texture desc. + const uint32_t mip_width = std::max(t->desc.width >> mip, 1u); + const uint32_t mip_height = std::max(t->desc.height >> mip, 1u); + VkBufferImageCopy region = {}; + region.bufferOffset = src_offset; + // bufferRowLength is in TEXELS (a block-multiple for BC). + region.bufferRowLength = row_pitch / bytes_per_block * block_width; + region.bufferImageHeight = height; // texels, block-aligned as given + region.imageSubresource.aspectMask = t->aspect; + region.imageSubresource.mipLevel = mip; + region.imageSubresource.baseArrayLayer = array_slice; + region.imageSubresource.layerCount = 1; + region.imageExtent.width = std::min(width, mip_width); + region.imageExtent.height = std::min(height, mip_height); + region.imageExtent.depth = depth; + device->cp()->deferred_command_buffer().CmdVkCopyBufferToImage( + b->buffer, t->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); +} + +void NrCmdVulkan::CopyTextureToBuffer(nrhi::Buffer* dst, uint64_t dst_offset, uint32_t row_pitch, + nrhi::Texture* src, uint32_t mip, uint32_t width, + uint32_t height) { + NrProfScope prof_scope(device->prof_.copies); + auto* t = static_cast(src); + auto* b = static_cast(dst); + EndRenderPassIfOpen(); + device->EnsureLayout(t, ToStateInfo(ResourceState::kCopySource)); + device->cp()->SubmitBarriers(true); + + const uint32_t block_width = nrhi::FormatBlockWidth(t->desc.format); + const uint32_t bytes_per_block = FormatBytesPerBlock(t->desc.format); + VkBufferImageCopy region = {}; + region.bufferOffset = dst_offset; + region.bufferRowLength = row_pitch / bytes_per_block * block_width; + region.bufferImageHeight = height; + region.imageSubresource.aspectMask = t->aspect; + region.imageSubresource.mipLevel = mip; + region.imageSubresource.baseArrayLayer = 0; + region.imageSubresource.layerCount = 1; + region.imageExtent.width = width; + region.imageExtent.height = height; + region.imageExtent.depth = 1; + device->cp()->deferred_command_buffer().CmdVkCopyImageToBuffer( + t->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, b->buffer, 1, ®ion); + // Order the transfer write against mapped host reads: the readback gate is + // the completed-submission counter, the memory dependency is this barrier + // (submitted with the next barrier flush / frame end). + device->cp()->PushBufferMemoryBarrier(b->buffer, dst_offset, uint64_t(row_pitch) * height, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, + VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); +} + +void NrCmdVulkan::Barrier(nrhi::Texture* texture, ResourceState before, ResourceState after) { + auto* t = static_cast(texture); + const StateInfo dst = ToStateInfo(after); + // A latched clear must not be lost when the texture leaves its attachment + // state without a draw having consumed it ("clear then barrier"): flush it + // as an empty CLEAR pass now, before the transition is queued. + if (t->pending_clear && after != ResourceState::kRenderTarget && + after != ResourceState::kDepthWrite) { + EndRenderPassIfOpen(); + const bool is_depth = t->aspect == VK_IMAGE_ASPECT_DEPTH_BIT; + device->EnsureLayout(t, ToStateInfo(is_depth ? ResourceState::kDepthWrite + : ResourceState::kRenderTarget)); + device->cp()->SubmitBarriers(true); + VkRenderPass render_pass = device->GetRenderPass( + is_depth ? VK_FORMAT_UNDEFINED : t->vk_format, + is_depth ? t->vk_format : VK_FORMAT_UNDEFINED, t->desc.sample_count, !is_depth, is_depth); + VkImageView view = device->GetAttachmentView(t); + if (render_pass != VK_NULL_HANDLE && view != VK_NULL_HANDLE) { + VkFramebuffer framebuffer = + device->GetFramebuffer(render_pass, is_depth ? VK_NULL_HANDLE : view, + is_depth ? view : VK_NULL_HANDLE, t->desc.width, t->desc.height); + if (framebuffer != VK_NULL_HANDLE) { + VkClearValue clear_value; + if (is_depth) { + clear_value.depthStencil = {t->clear_depth, 0}; + } else { + std::memcpy(clear_value.color.float32, t->clear_color, sizeof(t->clear_color)); + } + VkRenderPassBeginInfo begin_info = {}; + begin_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + begin_info.renderPass = render_pass; + begin_info.framebuffer = framebuffer; + begin_info.renderArea.extent = {t->desc.width, t->desc.height}; + begin_info.clearValueCount = 1; + begin_info.pClearValues = &clear_value; + DeferredCommandBuffer& cmd = device->cp()->deferred_command_buffer(); + cmd.CmdVkBeginRenderPass(&begin_info, VK_SUBPASS_CONTENTS_INLINE); + cmd.CmdVkEndRenderPass(); + } + } + t->pending_clear = false; + std::erase(device->pending_clear_textures(), t); + } + if (t->current_layout == dst.layout) { + // Layout already matches (e.g. the internal ensure-layout ran) - still + // queue an execution/memory dependency between the states. + const StateInfo src = ToStateInfo(before); + device->cp()->PushImageMemoryBarrier(t->image, t->WholeRange(), src.stage_mask, dst.stage_mask, + src.access_mask, dst.access_mask, dst.layout, dst.layout); + return; + } + const StateInfo src = ToStateInfo(before); + const bool undefined = t->current_layout == VK_IMAGE_LAYOUT_UNDEFINED; + device->cp()->PushImageMemoryBarrier( + t->image, t->WholeRange(), + undefined ? VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT : src.stage_mask, dst.stage_mask, + undefined ? 0 : src.access_mask, dst.access_mask, + // The tracked layout is the authoritative oldLayout: first-use + // transitions come from UNDEFINED and discard. + t->current_layout, dst.layout); + t->current_layout = dst.layout; +} + +void NrCmdVulkan::FlushBarriers() { + NrProfScope prof_scope(device->prof_.flush_barriers); + // My render passes live in the same deferred command buffer the CP records + // barriers into - close mine BEFORE the barrier commands are recorded (the + // CP's SubmitBarriers only closes its own tracked pass). + EndRenderPassIfOpen(); + device->cp()->SubmitBarriers(true); +} + +} // namespace + +// --------------------------------------------------------------------------- +// Public factory / per-frame entry points. +// --------------------------------------------------------------------------- + +nrhi::Device* CreateNativeRhiDevice(VulkanCommandProcessor* command_processor) { + return new NrDeviceVulkan(command_processor); +} + +void DestroyNativeRhiDevice(nrhi::Device* device) { + delete static_cast(device); +} + +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) { + return static_cast(device)->BeginFrame( + guest_output_image, guest_output_image_view, guest_output_ever_written_previously, width, + height, guest_output_out); +} + +void NativeRhiEndFrame(nrhi::Device* device) { + static_cast(device)->EndFrame(); +} + +} // namespace rex::graphics::vulkan