summaryrefslogtreecommitdiffstats
path: root/quantum/painter/qp_draw_core.c
blob: aa5fa4aa761f8b9a6a0a520776f1992c3953f008 (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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
// Copyright 2021-2022 Nick Brassel (@tzarc)
// Copyright 2021 Paul Cotter (@gr1mr3aver)
// SPDX-License-Identifier: GPL-2.0-or-later

#include "qp_internal.h"
#include "qp_comms.h"
#include "qp_draw.h"
#include "qgf.h"

_Static_assert((QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE > 0) && (QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE % 16) == 0, "QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE needs to be a non-zero multiple of 16");

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Global variables
//
// NOTE: The variables in this section are intentionally outside a stack frame. They are able to be defined with larger
//       sizes than the normal stack frames would allow, and as such need to be external.
//
//       **** DO NOT refactor this and decide to place the variables inside the function calling them -- you will ****
//       **** very likely get artifacts rendered to the screen as a result.                                       ****
//

// Buffer used for transmitting native pixel data to the downstream device.
__attribute__((__aligned__(4))) uint8_t qp_internal_global_pixdata_buffer[QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE];

// Static buffer to contain a generated color palette
static bool                                       generated_palette = false;
static int16_t                                    generated_steps   = -1;
__attribute__((__aligned__(4))) static qp_pixel_t interpolated_fg_hsv888;
__attribute__((__aligned__(4))) static qp_pixel_t interpolated_bg_hsv888;
#if QUANTUM_PAINTER_SUPPORTS_256_PALETTE
__attribute__((__aligned__(4))) qp_pixel_t qp_internal_global_pixel_lookup_table[256];
#else
__attribute__((__aligned__(4))) qp_pixel_t qp_internal_global_pixel_lookup_table[16];
#endif

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Helpers

uint32_t qp_internal_num_pixels_in_buffer(painter_device_t device) {
    painter_driver_t *driver = (painter_driver_t *)device;
    return ((QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE * 8) / driver->native_bits_per_pixel);
}

// qp_setpixel internal implementation, but accepts a buffer with pre-converted native pixel. Only the first pixel is used.
bool qp_internal_setpixel_impl(painter_device_t device, uint16_t x, uint16_t y) {
    painter_driver_t *driver = (painter_driver_t *)device;
    return driver->driver_vtable->viewport(device, x, y, x, y) && driver->driver_vtable->pixdata(device, qp_internal_global_pixdata_buffer, 1);
}

// Fills the global native pixel buffer with equivalent pixels matching the supplied HSV
void qp_internal_fill_pixdata(painter_device_t device, uint32_t num_pixels, uint8_t hue, uint8_t sat, uint8_t val) {
    painter_driver_t *driver            = (painter_driver_t *)device;
    uint32_t          pixels_in_pixdata = qp_internal_num_pixels_in_buffer(device);
    num_pixels                          = QP_MIN(pixels_in_pixdata, num_pixels);

    // Convert the color to native pixel format
    qp_pixel_t color = {.hsv888 = {.h = hue, .s = sat, .v = val}};
    driver->driver_vtable->palette_convert(device, 1, &color);

    // Append the required number of pixels
    uint8_t palette_idx = 0;
    for (uint32_t i = 0; i < num_pixels; ++i) {
        driver->driver_vtable->append_pixels(device, qp_internal_global_pixdata_buffer, &color, i, 1, &palette_idx);
    }
}

// Resets the global palette so that it can be regenerated. Only needed if the colors are identical, but a different display is used with a different internal pixel format.
void qp_internal_invalidate_palette(void) {
    generated_palette = false;
    generated_steps   = -1;
}

// Interpolates between two colors to generate a palette
bool qp_internal_interpolate_palette(qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888, int16_t steps) {
    // Check if we need to generate a new palette -- if the input parameters match then assume the palette can stay unchanged.
    // This may present a problem if using the same parameters but a different screen converts pixels -- use qp_internal_invalidate_palette() to reset.
    if (generated_palette == true && generated_steps == steps && memcmp(&interpolated_fg_hsv888, &fg_hsv888, sizeof(fg_hsv888)) == 0 && memcmp(&interpolated_bg_hsv888, &bg_hsv888, sizeof(bg_hsv888)) == 0) {
        // We already have the correct palette, no point regenerating it.
        return false;
    }

    // Save the parameters so we know whether we can skip generation
    generated_palette      = true;
    generated_steps        = steps;
    interpolated_fg_hsv888 = fg_hsv888;
    interpolated_bg_hsv888 = bg_hsv888;

    int16_t hue_fg = fg_hsv888.hsv888.h;
    int16_t hue_bg = bg_hsv888.hsv888.h;

    // Make sure we take the "shortest" route from one hue to the other
    if ((hue_fg - hue_bg) >= 128) {
        hue_bg += 256;
    } else if ((hue_fg - hue_bg) <= -128) {
        hue_bg -= 256;
    }

    // Interpolate each of the lookup table entries
    for (int16_t i = 0; i < steps; ++i) {
        qp_internal_global_pixel_lookup_table[i].hsv888.h = (uint8_t)((hue_fg - hue_bg) * i / (steps - 1) + hue_bg);
        qp_internal_global_pixel_lookup_table[i].hsv888.s = (uint8_t)((fg_hsv888.hsv888.s - bg_hsv888.hsv888.s) * i / (steps - 1) + bg_hsv888.hsv888.s);
        qp_internal_global_pixel_lookup_table[i].hsv888.v = (uint8_t)((fg_hsv888.hsv888.v - bg_hsv888.hsv888.v) * i / (steps - 1) + bg_hsv888.hsv888.v);

        qp_dprintf("qp_internal_interpolate_palette: %3d of %d -- H: %3d, S: %3d, V: %3d\n", (int)(i + 1), (int)steps, (int)qp_internal_global_pixel_lookup_table[i].hsv888.h, (int)qp_internal_global_pixel_lookup_table[i].hsv888.s, (int)qp_internal_global_pixel_lookup_table[i].hsv888.v);
    }

    return true;
}

// Helper shared between image and font rendering -- sets up the global palette to match the palette block specified in the asset. Expects the stream to be positioned at the start of the block header.
bool qp_internal_load_qgf_palette(qp_stream_t *stream, uint8_t bpp) {
    qgf_palette_v1_t palette_descriptor;
    if (qp_stream_read(&palette_descriptor, sizeof(qgf_palette_v1_t), 1, stream) != 1) {
        qp_dprintf("Failed to read palette_descriptor, expected length was not %d\n", (int)sizeof(qgf_palette_v1_t));
        return false;
    }

    // BPP determines the number of palette entries, each entry is a HSV888 triplet.
    const uint16_t palette_entries = 1u << bpp;

    // Ensure we aren't reusing any palette
    qp_internal_invalidate_palette();

    // Read the palette entries
    for (uint16_t i = 0; i < palette_entries; ++i) {
        // Read the palette entry
        qgf_palette_entry_v1_t entry;
        if (qp_stream_read(&entry, sizeof(qgf_palette_entry_v1_t), 1, stream) != 1) {
            return false;
        }

        // Update the lookup table
        qp_internal_global_pixel_lookup_table[i].hsv888.h = entry.h;
        qp_internal_global_pixel_lookup_table[i].hsv888.s = entry.s;
        qp_internal_global_pixel_lookup_table[i].hsv888.v = entry.v;

        qp_dprintf("qp_internal_load_qgf_palette: %3d of %d -- H: %3d, S: %3d, V: %3d\n", (int)(i + 1), (int)palette_entries, (int)qp_internal_global_pixel_lookup_table[i].hsv888.h, (int)qp_internal_global_pixel_lookup_table[i].hsv888.s, (int)qp_internal_global_pixel_lookup_table[i].hsv888.v);
    }

    return true;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_setpixel

bool qp_setpixel(painter_device_t device, uint16_t x, uint16_t y, uint8_t hue, uint8_t sat, uint8_t val) {
    painter_driver_t *driver = (painter_driver_t *)device;
    if (!driver || !driver->validate_ok) {
        qp_dprintf("qp_setpixel: fail (validation_ok == false)\n");
        return false;
    }

    if (!qp_comms_start(device)) {
        qp_dprintf("Failed to start comms in qp_setpixel\n");
        return false;
    }

    qp_internal_fill_pixdata(device, 1, hue, sat, val);
    bool ret = qp_internal_setpixel_impl(device, x, y);
    qp_comms_stop(device);
    qp_dprintf("qp_setpixel: %s\n", ret ? "ok" : "fail");
    return ret;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_line

bool qp_line(painter_device_t device, uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint8_t hue, uint8_t sat, uint8_t val) {
    if (x0 == x1 || y0 == y1) {
        qp_dprintf("qp_line(%d, %d, %d, %d): entry (deferring to qp_rect)\n", (int)x0, (int)y0, (int)x1, (int)y1);
        bool ret = qp_rect(device, x0, y0, x1, y1, hue, sat, val, true);
        qp_dprintf("qp_line(%d, %d, %d, %d): %s (deferred to qp_rect)\n", (int)x0, (int)y0, (int)x1, (int)y1, ret ? "ok" : "fail");
        return ret;
    }

    qp_dprintf("qp_line(%d, %d, %d, %d): entry\n", (int)x0, (int)y0, (int)x1, (int)y1);
    painter_driver_t *driver = (painter_driver_t *)device;
    if (!driver || !driver->validate_ok) {
        qp_dprintf("qp_line: fail (validation_ok == false)\n");
        return false;
    }

    if (!qp_comms_start(device)) {
        qp_dprintf("Failed to start comms in qp_line\n");
        return false;
    }

    qp_internal_fill_pixdata(device, 1, hue, sat, val);

    // draw angled line using Bresenham's algo
    int16_t x      = ((int16_t)x0);
    int16_t y      = ((int16_t)y0);
    int16_t slopex = ((int16_t)x0) < ((int16_t)x1) ? 1 : -1;
    int16_t slopey = ((int16_t)y0) < ((int16_t)y1) ? 1 : -1;
    int16_t dx     = abs(((int16_t)x1) - ((int16_t)x0));
    int16_t dy     = -abs(((int16_t)y1) - ((int16_t)y0));

    int16_t e  = dx + dy;
    int16_t e2 = 2 * e;

    bool ret = true;
    while (x != x1 || y != y1) {
        if (!qp_internal_setpixel_impl(device, x, y)) {
            ret = false;
            break;
        }
        e2 = 2 * e;
        if (e2 >= dy) {
            e += dy;
            x += slopex;
        }
        if (e2 <= dx) {
            e += dx;
            y += slopey;
        }
    }
    // draw the last pixel
    if (!qp_internal_setpixel_impl(device, x, y)) {
        ret = false;
    }

    qp_comms_stop(device);
    qp_dprintf("qp_line(%d, %d, %d, %d): %s\n", (int)x0, (int)y0, (int)x1, (int)y1, ret ? "ok" : "fail");
    return ret;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_rect

bool qp_internal_fillrect_helper_impl(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) {
    uint32_t          pixels_in_pixdata = qp_internal_num_pixels_in_buffer(device);
    painter_driver_t *driver            = (painter_driver_t *)device;

    uint16_t l = QP_MIN(left, right);
    uint16_t r = QP_MAX(left, right);
    uint16_t t = QP_MIN(top, bottom);
    uint16_t b = QP_MAX(top, bottom);
    uint16_t w = r - l + 1;
    uint16_t h = b - t + 1;

    uint32_t remaining = w * h;
    driver->driver_vtable->viewport(device, l, t, r, b);
    while (remaining > 0) {
        uint32_t transmit = QP_MIN(remaining, pixels_in_pixdata);
        if (!driver->driver_vtable->pixdata(device, qp_internal_global_pixdata_buffer, transmit)) {
            return false;
        }
        remaining -= transmit;
    }
    return true;
}

bool qp_rect(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom, uint8_t hue, uint8_t sat, uint8_t val, bool filled) {
    qp_dprintf("qp_rect(%d, %d, %d, %d): entry\n", (int)left, (int)top, (int)right, (int)bottom);
    painter_driver_t *driver = (painter_driver_t *)device;
    if (!driver || !driver->validate_ok) {
        qp_dprintf("qp_rect: fail (validation_ok == false)\n");
        return false;
    }

    // Cater for cases where people have submitted the coordinates backwards
    uint16_t l = QP_MIN(left, right);
    uint16_t r = QP_MAX(left, right);
    uint16_t t = QP_MIN(top, bottom);
    uint16_t b = QP_MAX(top, bottom);
    uint16_t w = r - l + 1;
    uint16_t h = b - t + 1;

    bool ret = true;
    if (!qp_comms_start(device)) {
        qp_dprintf("Failed to start comms in qp_rect\n");
        return false;
    }

    if (filled) {
        // Fill up the pixdata buffer with the required number of native pixels
        qp_internal_fill_pixdata(device, w * h, hue, sat, val);

        // Perform the draw
        ret = qp_internal_fillrect_helper_impl(device, l, t, r, b);
    } else {
        // Fill up the pixdata buffer with the required number of native pixels
        qp_internal_fill_pixdata(device, QP_MAX(w, h), hue, sat, val);

        // Draw 4x filled single-width rects to create an outline
        if (!qp_internal_fillrect_helper_impl(device, l, t, r, t) || !qp_internal_fillrect_helper_impl(device, l, b, r, b) || !qp_internal_fillrect_helper_impl(device, l, t + 1, l, b - 1) || !qp_internal_fillrect_helper_impl(device, r, t + 1, r, b - 1)) {
            ret = false;
        }
    }

    qp_comms_stop(device);
    qp_dprintf("qp_rect(%d, %d, %d, %d): %s\n", (int)l, (int)t, (int)r, (int)b, ret ? "ok" : "fail");
    return ret;
}