#pragma once // Donut RHI (Render Hardware Interface): a small, portable GPU abstraction that // OpenGL, Vulkan (and later Metal / D3D12) implement behind ONE interface, so the // app's rendering — the black hole, the scene — is written ONCE on top and runs // on any backend. the shape is modelled on the explicit APIs (baked pipelines, // recorded command lists, explicit render targets); OpenGL emulates that, which // is easy, whereas the reverse (making Vulkan speak GL's immediate mode) is not. #include "core/memory.h" #include #include #include #include #include namespace Donut::RHI { enum class Format { None, // no attachment (e.g. a target/pipeline with no depth) Swapchain, // a pipeline's colour target = the presented image (resolved per backend) RGBA8, // 8-bit unorm colour (LDR / off-screen) RGBA16F, // half-float colour (HDR / cubemap) RGBA32F, // full-float colour (raw observable export) D32, // 32-bit depth }; // the attachment signature of a render pass / target: a colour format plus an // optional depth format. a pipeline is compatible with any target sharing this // signature, so this is what replaces "which render pass" as an explicit value. struct RenderTargetFormat { Format color = Format::RGBA8; Format depth = Format::None; }; // the window the device presents into. GLFW is the windowing layer for every // backend, so this carries the GLFWwindow*; each backend derives what it needs // (a GL context, a Vulkan surface, later a CAMetalLayer / HWND) from it. the // RHI names the concept "native window" rather than baking GLFW into its API. struct NativeWindow { void* glfw_handle = nullptr; int width = 0; int height = 0; }; enum class BufferType { Vertex, Index, Uniform }; enum class Topology { Triangles, Lines }; enum class CullMode { None, Back, Front }; enum class BlendMode { Opaque, AlphaBlend }; enum class CompareOp { Always, Less, LessEqual }; enum class Filter { Nearest, Linear }; // one vertex attribute; offsets/stride are in bytes. `components` is 1..4 floats. struct VertexAttribute { uint32_t location; uint32_t components; uint32_t offset; }; struct VertexLayout { uint32_t stride = 0; std::vector attributes; }; // a shader resource slot the pipeline exposes. `binding` is the set-0 binding // index used by Vulkan; `name` is the GLSL block/sampler identifier used by the // OpenGL backend (GL 4.1 has no binding qualifier, so it binds by name). the app // declares both when building a pipeline (it knows its own shader). enum class ResourceKind { UniformBuffer, Texture }; struct ResourceSlot { ResourceKind kind; uint32_t binding; std::string name; }; // opaque GPU resources; each backend subclasses these. class Buffer { public: virtual ~Buffer() = default; virtual auto update(const void* data, size_t size) -> void = 0; }; class Texture { public: virtual ~Texture() = default; }; class Pipeline{ public: virtual ~Pipeline() = default; }; // an off-screen target (colour, optional depth). the swapchain is the implicit // default target, addressed by passing nullptr to begin_render_pass. class RenderTarget { public: virtual ~RenderTarget() = default; virtual auto width() const -> int = 0; virtual auto height() const -> int = 0; virtual auto color_texture() -> Texture* = 0; // to sample this target's colour }; struct PipelineDesc { std::string shader; // base name; backend loads .spv or .glsl VertexLayout vertex_layout; std::vector resources; // UBO/texture slots the shader reads Topology topology = Topology::Triangles; CullMode cull = CullMode::None; BlendMode blend = BlendMode::Opaque; bool depth_test = false; bool depth_write = false; CompareOp depth_op = CompareOp::Less; RenderTargetFormat target; // attachment signature of the pass it draws into }; // records draws for one frame. obtained from Device::begin_frame (targets the // swapchain) or created transiently for off-screen passes via the Device. class CommandList { public: virtual ~CommandList() = default; // target == nullptr renders to the swapchain; otherwise to the RenderTarget. virtual auto begin_render_pass(RenderTarget* target, const glm::vec4& clear) -> void = 0; virtual auto end_render_pass() -> void = 0; virtual auto bind_pipeline(Pipeline* pipeline) -> void = 0; virtual auto set_viewport(int x, int y, int w, int h, bool flip_y = false) -> void = 0; virtual auto bind_uniform(uint32_t binding, Buffer* ubo) -> void = 0; virtual auto bind_texture(uint32_t binding, Texture* texture) -> void = 0; virtual auto bind_vertex_buffer(Buffer* vb) -> void = 0; virtual auto bind_index_buffer(Buffer* ib) -> void = 0; // 32-bit indices virtual auto draw(uint32_t vertex_count) -> void = 0; virtual auto draw_indexed(uint32_t index_count) -> void = 0; }; // the backend root: owns the device/swapchain and creates every resource. class Device { public: virtual ~Device() = default; virtual auto init(const NativeWindow& window) -> bool = 0; virtual auto shutdown() -> void = 0; virtual auto resize(int width, int height) -> void = 0; virtual auto wait_idle() -> void = 0; // present pacing: GL toggles the swap interval; Vulkan swaps the present // mode (FIFO vs IMMEDIATE) and rebuilds the swapchain. default: no-op. virtual auto set_vsync(bool enabled) -> void { (void)enabled; } virtual auto create_buffer(BufferType type, size_t size, const void* data = nullptr) -> Ref = 0; virtual auto create_texture(int width, int height, Format format, Filter filter, const void* data = nullptr) -> Ref = 0; virtual auto create_cubemap_from_hdri(const std::string& equirect_path) -> Ref = 0; virtual auto create_render_target(int width, int height, Format color, Format depth = Format::None, Filter sample_filter = Filter::Linear, int mip_levels = 1) -> Ref = 0; virtual auto create_pipeline(const PipelineDesc& desc) -> Ref = 0; // frame loop: begin_frame returns the frame's command list (or nullptr if // the frame is skipped, e.g. minimised); record one or more render passes // into it — off-screen passes into RenderTargets first, then the swapchain // pass (begin_render_pass(nullptr, ...)) — then end_frame submits + presents. virtual auto begin_frame(const glm::vec4& clear) -> CommandList* = 0; virtual auto end_frame() -> void = 0; // one-shot off-screen work outside the frame loop (used by the exporter): // records a self-contained pass into a transient command list and blocks // until the GPU finishes, so the target can be read back immediately. virtual auto run_offscreen(const std::function& record) -> void = 0; // reads a render target's colour back to the CPU as tightly-packed RGBA8, // top-left origin (out is resized to width*height*4). for export/analysis. virtual auto read_render_target(RenderTarget* target, std::vector& out) -> void = 0; // same, but as full-float RGBA (out resized to width*height*4 floats). read // an RGBA32F target back for raw observable export. virtual auto read_render_target_float(RenderTarget* target, std::vector& out) -> void = 0; // ImGui lives above the RHI but its platform/render backend is per-device. virtual auto init_imgui() -> void = 0; virtual auto imgui_new_frame() -> void = 0; virtual auto imgui_render(CommandList& swapchain_cmds) -> void = 0; virtual auto device_name() const -> const std::string& = 0; }; }