538 lines
18 KiB
C++
538 lines
18 KiB
C++
#pragma once
|
|
|
|
// Native-render RHI: the backend abstraction the app-side native scene
|
|
// renderer draws through. One implementation per SDK graphics backend
|
|
// (d3d12/native_rhi_d3d12.cpp, vulkan/native_rhi_vulkan.cpp), each built on
|
|
// that backend's command processor (deferred command list, barrier queue,
|
|
// submission counters).
|
|
//
|
|
// The semantics deliberately mirror the D3D12 model the scene renderer was
|
|
// written against: explicit resource states, latched command state, a
|
|
// binding model of root constants + texture tables + direct buffer binds;
|
|
// so the D3D12 backend is a thin passthrough and the Vulkan backend owns all
|
|
// translation (render-pass scoping, descriptor sets, layout transitions,
|
|
// the y-flip).
|
|
//
|
|
// Threading contract:
|
|
// - Create*/Destroy*/Map/Unmap/InvalidateForRead on Device are thread-safe
|
|
// (async decode workers create and fill resources off-thread).
|
|
// - CreateTextureView / CreateGraphicsPipeline / CreateBindingLayout /
|
|
// CreateShader and every Cmd method are render-thread-only.
|
|
// - Cmd is only valid inside the native guest-output render callback.
|
|
//
|
|
// Alignment contract: buffer-bind offsets (SetConstantBuffer/SetBufferSrv)
|
|
// and copy row pitches must be kRowPitchAlignment-aligned. Upload memory is
|
|
// write-combined: never read it back; readback memory must be read through
|
|
// InvalidateForRead.
|
|
|
|
#include <cstdint>
|
|
|
|
namespace rex::graphics::nrhi {
|
|
|
|
inline constexpr uint32_t kRowPitchAlignment = 256;
|
|
inline constexpr uint32_t kBufferOffsetAlignment = 256;
|
|
|
|
enum class Backend : uint32_t {
|
|
kD3D12,
|
|
kVulkan,
|
|
};
|
|
|
|
// Texture, view and vertex formats: exactly the set the scene renderer
|
|
// uses. kR10G10B10A2 is the guest-output format on both backends (identical
|
|
// bit layout: DXGI_FORMAT_R10G10B10A2_UNORM / VK_FORMAT_A2B10G10R10_UNORM_PACK32).
|
|
enum class Format : uint32_t {
|
|
kUnknown = 0,
|
|
kR8G8B8A8_UNORM,
|
|
kR8G8B8A8_UINT, // vertex blend indices
|
|
kR8_UNORM,
|
|
kR8G8_UNORM,
|
|
kB5G6R5_UNORM,
|
|
kR16G16_UNORM,
|
|
kR10G10B10A2_UNORM,
|
|
kR32_FLOAT,
|
|
kR32G32_FLOAT,
|
|
kR32G32B32_FLOAT, // vertex only
|
|
kR32G32B32A32_FLOAT, // vertex only
|
|
kD32_FLOAT,
|
|
kBC1_UNORM,
|
|
kBC2_UNORM,
|
|
kBC3_UNORM,
|
|
kBC4_UNORM,
|
|
kBC5_UNORM,
|
|
// Float color formats (HDR render targets), appended so existing values
|
|
// stay stable across exe/dll pairs. Both are color-renderable and
|
|
// blendable on every target backend (D3D12 FL11+, Vulkan core, MoltenVK);
|
|
// kR11G11B10_FLOAT has no alpha channel and stores no sign bits.
|
|
kR16G16B16A16_FLOAT,
|
|
kR11G11B10_FLOAT,
|
|
};
|
|
|
|
// 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.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Binding classes a buffer will be used with. kFull is the historical
|
|
// default (constant buffer / buffer SRV / vertex / index / copy source).
|
|
// Narrower classes let backends drop the descriptor-window padding that only
|
|
// dynamic-offset constant/SRV binds need, and request narrower usage.
|
|
enum class BufferBindClass : uint32_t {
|
|
kFull,
|
|
kVertexIndex, // vertex/index binds and copy source only
|
|
kCopySrc, // copy source only (texture upload staging)
|
|
};
|
|
|
|
struct BufferDesc {
|
|
uint64_t size = 0;
|
|
HeapKind heap = HeapKind::kDefault;
|
|
BufferBindClass bind_class = BufferBindClass::kFull;
|
|
};
|
|
|
|
struct TextureDesc {
|
|
TextureKind kind = TextureKind::k2D;
|
|
uint32_t width = 1;
|
|
uint32_t height = 1;
|
|
uint32_t depth = 1; // k3D only
|
|
uint32_t mip_levels = 1;
|
|
uint32_t sample_count = 1;
|
|
Format format = Format::kUnknown;
|
|
uint32_t usage = kTextureUsageNone; // TextureUsage bits
|
|
ResourceState initial_state = ResourceState::kCopyDest;
|
|
// Optimized clear value for render/depth targets (D3D12 fast clears).
|
|
float clear_color[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
|
float clear_depth = 1.0f;
|
|
};
|
|
|
|
struct TextureViewDesc {
|
|
ViewDimension dimension = ViewDimension::k2D;
|
|
// kUnknown = the texture's own format (D32_FLOAT textures are viewed as
|
|
// R32_FLOAT automatically).
|
|
Format format = Format::kUnknown;
|
|
uint32_t base_mip = 0;
|
|
uint32_t mip_levels = ~0u; // ~0u = all remaining
|
|
Swizzle swizzle[4] = {Swizzle::kX, Swizzle::kY, Swizzle::kZ, Swizzle::kW};
|
|
};
|
|
|
|
enum class BindingParamKind : uint32_t {
|
|
// 32-bit root constants (register b<slot>). One per layout at most. On
|
|
// Vulkan this is backed by an internal per-draw ring UBO.
|
|
kConstants = 0,
|
|
// Constant buffer bound by (buffer, offset).
|
|
kConstantBuffer,
|
|
// Read-only structured/raw buffer bound by (buffer, offset).
|
|
kBufferSrv,
|
|
// Table of 1..8 sampled textures at consecutive t-registers, bound with
|
|
// SetTexture / SetTexturePair / SetTextures.
|
|
kTextureTable,
|
|
};
|
|
|
|
inline constexpr uint32_t kMaxTextureTableSize = 8;
|
|
|
|
struct BindingParamDesc {
|
|
BindingParamKind kind = BindingParamKind::kTextureTable;
|
|
uint32_t shader_register = 0; // b/t register of the first entry
|
|
uint32_t count = 1; // kConstants: number of 32-bit values;
|
|
// kTextureTable: 1 or 2 textures
|
|
Visibility visibility = Visibility::kAll;
|
|
};
|
|
|
|
struct StaticSamplerDesc {
|
|
uint32_t shader_register = 0; // s register
|
|
Filter filter = Filter::kLinear;
|
|
AddressMode address = AddressMode::kClamp;
|
|
uint32_t max_anisotropy = 1;
|
|
};
|
|
|
|
inline constexpr uint32_t kMaxBindingParams = 12;
|
|
inline constexpr uint32_t kMaxStaticSamplers = 4;
|
|
|
|
struct BindingLayoutDesc {
|
|
BindingParamDesc params[kMaxBindingParams] = {};
|
|
uint32_t param_count = 0;
|
|
StaticSamplerDesc static_samplers[kMaxStaticSamplers] = {};
|
|
uint32_t static_sampler_count = 0;
|
|
// D3D12: whether the root signature allows an input-assembler layout.
|
|
bool allow_input_layout = true;
|
|
};
|
|
|
|
struct ShaderMacro {
|
|
const char* name = nullptr;
|
|
const char* value = nullptr;
|
|
};
|
|
|
|
struct ShaderDesc {
|
|
ShaderStage stage = ShaderStage::kVertex;
|
|
// Debug identity, used in compile-error logs.
|
|
const char* name = nullptr;
|
|
// HLSL source + entry point; the D3D12 backend compiles this at runtime
|
|
// (vs_5_0 / ps_5_0), exactly as the scene renderer always has.
|
|
const char* hlsl_source = nullptr;
|
|
const char* entry_point = nullptr;
|
|
const ShaderMacro* macros = nullptr; // terminated by a {nullptr, nullptr}
|
|
// Prebuilt SPIR-V for the same (source, entry, macros), generated
|
|
// offline by the app's shader script; required by the Vulkan backend.
|
|
const uint32_t* spirv = nullptr;
|
|
size_t spirv_size_bytes = 0;
|
|
};
|
|
|
|
struct InputElementDesc {
|
|
// D3D12 semantic binding.
|
|
const char* semantic_name = nullptr;
|
|
uint32_t semantic_index = 0;
|
|
// Vulkan input location: the element's declaration order in the VS input
|
|
// struct (dxc assigns locations in that order).
|
|
uint32_t location = 0;
|
|
Format format = Format::kUnknown;
|
|
uint32_t byte_offset = 0;
|
|
};
|
|
|
|
struct BlendStateDesc {
|
|
bool enable = false;
|
|
BlendFactor src = BlendFactor::kOne;
|
|
BlendFactor dst = BlendFactor::kZero;
|
|
BlendOp op = BlendOp::kAdd;
|
|
BlendFactor src_alpha = BlendFactor::kOne;
|
|
BlendFactor dst_alpha = BlendFactor::kZero;
|
|
BlendOp op_alpha = BlendOp::kAdd;
|
|
uint8_t write_mask = 0xF; // RGBA bits 0..3
|
|
};
|
|
|
|
struct DepthStateDesc {
|
|
bool test_enable = false;
|
|
bool write_enable = false;
|
|
CompareFunc func = CompareFunc::kLess;
|
|
};
|
|
|
|
struct GraphicsPipelineDesc {
|
|
BindingLayout* layout = nullptr;
|
|
Shader* vs = nullptr;
|
|
Shader* ps = nullptr;
|
|
const InputElementDesc* input_elements = nullptr; // nullptr = SV_VertexID
|
|
uint32_t input_element_count = 0;
|
|
uint32_t vertex_stride = 0;
|
|
BlendStateDesc blend;
|
|
DepthStateDesc depth;
|
|
CullMode cull = CullMode::kNone;
|
|
bool depth_clip = true;
|
|
Format rtv_format = Format::kUnknown; // kUnknown = no color target
|
|
Format dsv_format = Format::kUnknown; // kUnknown = no depth target
|
|
uint32_t sample_count = 1;
|
|
};
|
|
|
|
struct Viewport {
|
|
float x = 0.0f;
|
|
float y = 0.0f;
|
|
float width = 0.0f;
|
|
float height = 0.0f;
|
|
float min_depth = 0.0f;
|
|
float max_depth = 1.0f;
|
|
};
|
|
|
|
struct Rect {
|
|
int32_t left = 0;
|
|
int32_t top = 0;
|
|
int32_t right = 0;
|
|
int32_t bottom = 0;
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Command recording. Wraps the command processor's deferred command list for
|
|
// the current frame; valid only inside the render callback, render thread
|
|
// only. State latches like a D3D12 command list; SetBindingLayout resets
|
|
// root-constant and binding state.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
class Cmd {
|
|
public:
|
|
virtual void SetBindingLayout(BindingLayout* layout) = 0;
|
|
virtual void SetPipeline(Pipeline* pipeline) = 0;
|
|
|
|
// kConstants param. Values persist across pipeline changes within the
|
|
// current layout (D3D12 root-constant semantics).
|
|
virtual void SetRootConstants(uint32_t param, uint32_t count,
|
|
const void* values,
|
|
uint32_t dest_offset_in_values) = 0;
|
|
// offset must be kBufferOffsetAlignment-aligned.
|
|
virtual void SetConstantBuffer(uint32_t param, Buffer* buffer,
|
|
uint64_t offset) = 0;
|
|
virtual void SetBufferSrv(uint32_t param, Buffer* buffer,
|
|
uint64_t offset) = 0;
|
|
virtual void SetTexture(uint32_t param, TextureView* view) = 0;
|
|
// Multi-entry kTextureTable params. Binding fewer views than the table
|
|
// declares leaves the tail entries at a backend fallback (white);
|
|
// shaders only sample declared-but-unbound slots behind flags (the old
|
|
// D3D12 "dangling t5" arrangement, made explicit).
|
|
virtual void SetTexturePair(uint32_t param, TextureView* first,
|
|
TextureView* second) = 0;
|
|
virtual void SetTextures(uint32_t param, TextureView* const* views,
|
|
uint32_t count) = 0;
|
|
|
|
// Mip 0 of each. color/depth may each be nullptr.
|
|
virtual void SetRenderTargets(Texture* color, Texture* depth) = 0;
|
|
virtual void ClearRenderTarget(Texture* color, const float color4[4]) = 0;
|
|
virtual void ClearDepth(Texture* depth, float value) = 0;
|
|
|
|
virtual void SetViewport(const Viewport& viewport) = 0;
|
|
virtual void SetScissor(const Rect& rect) = 0;
|
|
|
|
virtual void SetVertexBuffer(Buffer* buffer, uint64_t offset,
|
|
uint32_t size_bytes, uint32_t stride) = 0;
|
|
// 16-bit indices (the only index format the scene uses).
|
|
virtual void SetIndexBuffer(Buffer* buffer, uint64_t offset,
|
|
uint32_t size_bytes) = 0;
|
|
virtual void SetPrimitiveTopology(PrimitiveTopology topology) = 0;
|
|
|
|
virtual void Draw(uint32_t vertex_count, uint32_t start_vertex) = 0;
|
|
virtual void DrawIndexed(uint32_t index_count, uint32_t start_index,
|
|
int32_t base_vertex) = 0;
|
|
|
|
// Copies. row_pitch must be kRowPitchAlignment-aligned. width/height are
|
|
// the explicit footprint dims in texels (block-aligned for BC formats,
|
|
// exactly as the D3D12 placed footprints were built); depth is 1 except
|
|
// for k3D uploads. array_slice selects the cube face for kCube textures.
|
|
virtual void CopyBufferToTexture(Texture* dst, uint32_t mip,
|
|
uint32_t array_slice, Buffer* src,
|
|
uint64_t src_offset, uint32_t row_pitch,
|
|
uint32_t width, uint32_t height,
|
|
uint32_t depth) = 0;
|
|
virtual void CopyTextureToBuffer(Buffer* dst, uint64_t dst_offset,
|
|
uint32_t row_pitch, Texture* src,
|
|
uint32_t mip, uint32_t width,
|
|
uint32_t height) = 0;
|
|
|
|
// Queues a transition barrier; FlushBarriers() submits queued barriers
|
|
// (and, on Vulkan, ends any open render pass).
|
|
virtual void Barrier(Texture* texture, ResourceState before,
|
|
ResourceState after) = 0;
|
|
virtual void FlushBarriers() = 0;
|
|
|
|
protected:
|
|
virtual ~Cmd() = default;
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Device: creation, destruction (always deferred), submission timeline.
|
|
// Owned by the command processor; outlives every frame's Cmd.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
class Device {
|
|
public:
|
|
virtual Backend backend() const = 0;
|
|
|
|
// --- thread-safe ---
|
|
virtual Buffer* CreateBuffer(const BufferDesc& desc) = 0;
|
|
virtual Texture* CreateTexture(const TextureDesc& desc) = 0;
|
|
// Persistent mapping is allowed (and the norm). Upload memory is
|
|
// write-combined: write-only, no reads.
|
|
virtual void* Map(Buffer* buffer) = 0;
|
|
virtual void Unmap(Buffer* buffer) = 0;
|
|
// Make GPU writes visible before reading mapped readback memory
|
|
// (no-op on D3D12, cache invalidate on non-coherent Vulkan memory).
|
|
virtual void InvalidateForRead(Buffer* buffer, uint64_t offset,
|
|
uint64_t size) = 0;
|
|
|
|
// Deferred destruction: the object dies when the current submission
|
|
// completes. Views/bindings referencing a destroyed object are retired by
|
|
// the backend. Null is allowed (no-op).
|
|
virtual void DestroyDeferred(Buffer* buffer) = 0;
|
|
virtual void DestroyDeferred(Texture* texture) = 0;
|
|
virtual void DestroyDeferred(TextureView* view) = 0;
|
|
virtual void DestroyDeferred(Pipeline* pipeline) = 0;
|
|
virtual void DestroyDeferred(Shader* shader) = 0;
|
|
|
|
// --- render-thread-only ---
|
|
virtual TextureView* CreateTextureView(Texture* texture,
|
|
const TextureViewDesc& desc) = 0;
|
|
virtual BindingLayout* CreateBindingLayout(const BindingLayoutDesc& desc) = 0;
|
|
// Returns nullptr on compile failure (details logged).
|
|
virtual Shader* CreateShader(const ShaderDesc& desc) = 0;
|
|
virtual Pipeline* CreateGraphicsPipeline(const GraphicsPipelineDesc& desc) = 0;
|
|
|
|
// Largest supported sample count <= desired for a render-target format.
|
|
virtual uint32_t GetSupportedSampleCount(Format format,
|
|
uint32_t desired) = 0;
|
|
|
|
// The command processor's monotonic submission counters: the basis of all
|
|
// lifetime/readback gating.
|
|
virtual uint64_t CurrentSubmission() const = 0;
|
|
virtual uint64_t CompletedSubmission() const = 0;
|
|
|
|
protected:
|
|
virtual ~Device() = default;
|
|
};
|
|
|
|
// Optional on-disk cache directory for runtime-compiled shader bytecode
|
|
// (the D3D12 backend's D3DCompile output; backends consuming prebuilt
|
|
// shaders ignore it). Entries are keyed by a content hash of the stage,
|
|
// entry point, macros and full source text, so source edits miss naturally
|
|
// and stale bytecode is never served. Set once during app startup, before
|
|
// the first CreateShader; empty (the default) disables caching. A missing
|
|
// or unwritable directory only costs compile time, never correctness.
|
|
void SetShaderBytecodeCacheDirectory(const char* path);
|
|
// The configured directory, or an empty string when caching is disabled.
|
|
const char* GetShaderBytecodeCacheDirectory();
|
|
|
|
} // namespace rex::graphics::nrhi
|