File size: 7,373 Bytes
3ce1a40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
171
172
173
174
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include "../core/math.h"
#include "../core/handle.h"

namespace unify {

struct RectI { int x = 0, y = 0, w = 0, h = 0; bool empty() const { return w <= 0 || h <= 0; } };
struct RectF { float x = 0, y = 0, w = 0, h = 0; };

/// CPU-side RGBA8 image. Also the storage of the software render target.
struct Image {
    int width = 0, height = 0;
    std::vector<uint8_t> pixels;  // RGBA, row-major, top row first
    Image() = default;
    Image(int w, int h, Color fill = {0, 0, 0, 0}) : width(w), height(h), pixels(size_t(w) * h * 4) {
        for (size_t i = 0; i < pixels.size(); i += 4) { pixels[i] = fill.r; pixels[i + 1] = fill.g; pixels[i + 2] = fill.b; pixels[i + 3] = fill.a; }
    }
    uint8_t* at(int x, int y) { return &pixels[(size_t(y) * width + x) * 4]; }
    const uint8_t* at(int x, int y) const { return &pixels[(size_t(y) * width + x) * 4]; }
};

bool decode_qoi(const std::vector<uint8_t>& data, Image& out);
std::vector<uint8_t> encode_qoi(const Image& img);
bool write_png(const std::string& path, const Image& img);   // uncompressed deflate, for screenshots
bool load_image(const std::string& path, Image& out);         // .qoi

enum class BlendMode : uint8_t { Opaque, Alpha, Additive, Multiply };

struct Material {
    BlendMode blend = BlendMode::Alpha;
    bool operator==(const Material& o) const { return blend == o.blend; }
};

/// Texture atlas: named sub-rectangles of one texture, packed with a shelf packer.
struct AtlasRegion { std::string name; RectI rect; };
struct Atlas {
    std::vector<AtlasRegion> regions;  // region 0 is the whole texture
    int find(const std::string& name) const {
        for (size_t i = 0; i < regions.size(); i++) if (regions[i].name == name) return int(i);
        return -1;
    }
};
/// Packs images into one atlas image; returns false if they do not fit.
bool pack_atlas(const std::vector<std::pair<std::string, const Image*>>& items, int size, Image& out, Atlas& atlas, int padding = 1);

struct Camera {
    Vec2 position;             // world point at the viewport centre
    float zoom = 32;           // pixels per world unit
    float rotation = 0;
};

struct Viewport { RectI rect; };

/// A render target: a colour buffer the device draws into.
struct RenderTarget {
    Image color;
    RenderTarget() = default;
    RenderTarget(int w, int h) : color(w, h, {0, 0, 0, 255}) {}
    int width() const { return color.width; }
    int height() const { return color.height; }
};

/// One queued draw. Everything is resolved to target pixels by the Renderer2D before
/// reaching the device, so devices only rasterize textured quads.
struct RenderCommand {
    uint32_t texture = 0;       // TextureHandle bits (0 = white)
    RectF src;                  // texel rectangle
    Vec2 corners[4];            // screen-space, CCW from bottom-left of the sprite
    Color tint;
    int16_t layer = 0;
    float depth = 0;            // within a layer: larger draws later (in front)
    Material material;
    RectI scissor;              // empty = full viewport
    uint32_t sequence = 0;      // submission order: final tie-breaker, keeps sort stable
};

struct Vertex { float x, y, u, v; Color color; };

/// A run of commands sharing texture, material and scissor: one device draw call.
struct Batch {
    uint32_t texture = 0;
    Material material;
    RectI scissor;
    const Vertex* vertices = nullptr;   // 4 per quad
    uint32_t quad_count = 0;
};

/// Abstract GPU-or-CPU device. Backends implement this (software, SDL, Unreal).
class RenderDevice {
public:
    virtual ~RenderDevice() = default;
    virtual TextureHandle create_texture(const Image& image) = 0;
    virtual bool update_texture(TextureHandle h, const Image& image) = 0;
    virtual bool destroy_texture(TextureHandle h) = 0;
    virtual bool texture_size(TextureHandle h, int& w, int& h_out) const = 0;
    virtual void begin(RenderTarget& target, Color clear) = 0;
    virtual void draw(const Batch& batch) = 0;
    virtual void end() = 0;
    virtual uint32_t live_textures() const = 0;
};

/// Reference device: rasterizes batches into the target's colour buffer on the CPU.
class SoftwareRenderDevice : public RenderDevice {
public:
    TextureHandle create_texture(const Image& image) override;
    bool update_texture(TextureHandle h, const Image& image) override;
    bool destroy_texture(TextureHandle h) override;
    bool texture_size(TextureHandle h, int& w, int& h_out) const override;
    void begin(RenderTarget& target, Color clear) override;
    void draw(const Batch& batch) override;
    void end() override {}
    uint32_t live_textures() const override { return handles_.alive(); }
private:
    void raster_triangle(const Vertex& a, const Vertex& b, const Vertex& c, const Image* tex, BlendMode blend, const RectI& clip);
    HandlePool<TextureHandle> handles_;
    std::vector<Image> textures_;
    RenderTarget* target_ = nullptr;
    Image white_{1, 1, {255, 255, 255, 255}};
};

struct RenderStats { uint32_t commands = 0, batches = 0, quads = 0; };

/// Collects commands for a frame, sorts and batches them, and submits to a device.
class Renderer2D {
public:
    explicit Renderer2D(RenderDevice& device) : device_(device) { commands_.reserve(4096); vertices_.reserve(4096 * 4); }

    void begin(RenderTarget& target, const Camera& camera, const Viewport& vp, Color clear = {20, 22, 30, 255});
    /// World-space sprite: centre position, size in world units, rotation.
    void sprite(uint32_t texture, RectF src, Vec2 pos, Vec2 size, float rotation, Color tint, int16_t layer, float depth,
                BlendMode blend = BlendMode::Alpha, bool flip_x = false, Vec2 pivot = {0.5f, 0.5f});
    /// Screen-space rectangle (UI): pixels, origin top-left.
    void screen_rect(uint32_t texture, RectF src, RectF dst, Color tint, int16_t layer, BlendMode blend = BlendMode::Alpha);
    void fill_screen_rect(RectF dst, Color c, int16_t layer) { screen_rect(0, {0, 0, 1, 1}, dst, c, layer); }
    void set_scissor(RectI r) { scissor_ = r; }
    void clear_scissor() { scissor_ = {}; }
    void end();

    Vec2 world_to_screen(Vec2 w) const;
    Vec2 screen_to_world(Vec2 s) const;
    const RenderStats& stats() const { return stats_; }
    /// Stats of the last completed frame (valid while a new frame is being built).
    const RenderStats& last_frame_stats() const { return last_stats_; }
    RenderDevice& device() { return device_; }

private:
    RenderDevice& device_;
    RenderTarget* target_ = nullptr;
    Camera camera_;
    Viewport viewport_;
    Color clear_;
    RectI scissor_;
    std::vector<RenderCommand> commands_;
    std::vector<Vertex> vertices_;
    uint32_t sequence_ = 0;
    RenderStats stats_, last_stats_;
};

/// Bitmap font (5x7 glyphs baked into an atlas-ready image, 6x8 cells).
struct BitmapFont {
    Image image;                   // 16x6 grid of 6x8 cells for ASCII 32..127
    uint32_t texture = 0;
    static BitmapFont build();
    RectF glyph(char c) const;
    /// Draws text at a screen position (top-left), scale = pixels per font pixel.
    void draw(Renderer2D& r, const std::string& text, Vec2 pos, float scale, Color color, int16_t layer) const;
    float width(const std::string& text, float scale) const { return float(text.size()) * 6 * scale; }
};

}  // namespace unify