aboutsummaryrefslogtreecommitdiff
path: root/src/rendering/rhi.h
blob: 6a2042af2047ae1ffe5be85b9a4b4a7ec0f8326b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#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 <glm/glm.hpp>
#include <cstdint>
#include <string>
#include <vector>
#include <functional>

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<VertexAttribute> 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 (backends subclass) --------------------------
    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<ResourceSlot> 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<Buffer> = 0;
        virtual auto create_texture(int width, int height, Format format, Filter filter, const void* data = nullptr) -> Ref<Texture> = 0;
        virtual auto create_cubemap_from_hdri(const std::string& equirect_path) -> Ref<Texture> = 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<RenderTarget> = 0;
        virtual auto create_pipeline(const PipelineDesc& desc) -> Ref<Pipeline> = 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<void(CommandList&)>& 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<uint8_t>& 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<float>& 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;
    };
}