| /* |
| * Copyright (C) 2026 Behdad Esfahbod |
| * |
| * This is part of HarfBuzz, a text shaping library. |
| * |
| * Permission is hereby granted, without written agreement and without |
| * license or royalty fees, to use, copy, modify, and distribute this |
| * software and its documentation for any purpose, provided that the |
| * above copyright notice and the following two paragraphs appear in |
| * all copies of this software. |
| * |
| * IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR |
| * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES |
| * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN |
| * IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH |
| * DAMAGE. |
| * |
| * THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, |
| * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND |
| * FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS |
| * ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO |
| * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. |
| * |
| * Author(s): Behdad Esfahbod |
| */ |
| |
| #ifndef HB_GPU_PAINT_HH |
| #define HB_GPU_PAINT_HH |
| |
| #include "hb.hh" |
| #include "hb-gpu.h" |
| #include "hb-geometry.hh" |
| #include "hb-map.hh" |
| #include "hb-object.hh" |
| |
| |
| /* |
| * hb_gpu_paint_t encoded-blob design |
| * ================================== |
| * |
| * One blob per glyph. One quad per glyph. The fragment shader |
| * is the interpreter: it walks a flat op stream per pixel, maintains |
| * a small group stack, and composites the result. |
| * |
| * The encoder turns a COLRv1 paint tree (visited via hb_paint_funcs_t |
| * callbacks) into a pre-order op stream plus embedded sub-payloads, |
| * all packed into a single RGBA16I blob suitable for |
| * hb_blob_t-style upload into the same shared atlas used by the |
| * draw renderer. |
| * |
| * --- Top-level layout -------------------------------------------- |
| * |
| * [header] |
| * [op stream] -- variable number of op records |
| * [sub-payloads] -- clip-glyph Slug draw blobs and |
| * gradient parameter blobs, referenced |
| * by intra-blob offsets from the ops |
| * |
| * Every offset in the blob is a texel index (RGBA16I, 8 bytes per |
| * texel) relative to the blob's base address. The caller uploads |
| * the blob at some atlas_base offset and adds that base to the |
| * glyph's vertex attribute; the fragment shader sees one absolute |
| * atlas offset per glyph and discovers everything else by walking |
| * inside. |
| * |
| * --- Header ------------------------------------------------------- |
| * |
| * texel 0: (num_ops:i16, reserved:i16, reserved:i16, reserved:i16) |
| * texel 1: (ext_min_x:i16, ext_min_y:i16, |
| * ext_max_x:i16, ext_max_y:i16) -- in HB_GPU units |
| * texel 2: (ops_offset:i16, reserved:i16, |
| * reserved:i16, reserved:i16) |
| * |
| * --- Op record ---------------------------------------------------- |
| * |
| * Each op is exactly one RGBA16I texel (4 int16s): |
| * |
| * (op_type:i16, op_aux:i16, payload_hi:i16, payload_lo:i16) |
| * |
| * op_type values: |
| * |
| * 0 LAYER_SOLID |
| * aux = flags (bit 0 = is_foreground) |
| * payload = clip_glyph_offset (texel index) |
| * followed by one extra texel: |
| * (r_q15:i16, g_q15:i16, b_q15:i16, a_q15:i16) |
| * flags bit 0: is_foreground -- use the shader's foreground |
| * uniform/varying instead of the baked color |
| * (so runtime foreground changes still work) |
| * |
| * 1 LAYER_GRADIENT |
| * aux = gradient subtype (0=linear, 1=radial, 2=sweep) |
| * payload = clip_glyph_offset (texel index) |
| * followed by one extra texel: |
| * (grad_offset_hi:i16, grad_offset_lo:i16, |
| * reserved:i16, reserved:i16) |
| * |
| * 2 PUSH_GROUP |
| * aux = 0 |
| * payload = 0 |
| * |
| * 3 POP_GROUP |
| * aux = composite mode, enumerated 0..27 matching |
| * hb_paint_composite_mode_t |
| * payload = 0 |
| * |
| * The fragment shader reads the first texel, inspects op_type, |
| * and knows from the type whether a trailing texel follows. Ops |
| * are therefore mixed-size but self-delimiting. |
| * |
| * --- Sub-payload: clip-glyph Slug draw blob ---------------------- |
| * |
| * Exactly the format produced by hb_gpu_draw_encode(). Referenced |
| * by LAYER_SOLID and LAYER_GRADIENT. |
| * |
| * The outline fed into the draw encoder has the enclosing paint |
| * transform baked in at CPU encode time. v1 does not dedup |
| * outlines that differ only by translation; v2 may keep the outline |
| * in canonical pre-translate space and store a per-op (dx, dy) |
| * offset in the aux/payload slots. |
| * |
| * --- Sub-payload: gradient parameter blob ------------------------ |
| * |
| * texels 0..2 : inverse affine transform, 6 components as |
| * q15.16 fixed point (two int16 per component, |
| * int part then fraction) |
| * texel 3 : (grad_subtype:i16, extend_mode:i16, |
| * reserved:i16, reserved:i16) |
| * texels 4..N : subtype-specific params |
| * linear: (x0, y0, px, py) -- two texels |
| * where p is the foot of perpendicular |
| * from p2 onto the p0..p1 line |
| * radial: (x0, y0, x1, y1), (r0, r1, _, _) |
| * sweep: (x0, y0, start_q14, end_q14) |
| * angles in q14 fractions of pi |
| * texels ... : color stops, each one texel: |
| * (offset_q8<<1 | last_stop_bit, |
| * palette_index:i16, |
| * flags:i16, |
| * alpha_q15:i16) |
| * flags / alpha_q15 semantics match LAYER_SOLID. |
| * The last stop is marked by the LSB of |
| * offset_q8 being 1 (offsets quantized to q8 so |
| * the bit is free for the marker). |
| * |
| * --- Shader entry point ----------------------------------------- |
| * |
| * vec4 hb_gpu_paint (vec2 renderCoord, uint glyphLoc, |
| * vec4 foreground); |
| * |
| * Returns premultiplied RGBA. The caller passes foreground in |
| * explicitly; sourcing it from a uniform, a per-quad flat varying, |
| * or anywhere else is the caller's decision. |
| * |
| * Colors are baked into the blob at encode time (as signed Q15 |
| * RGBA in a single texel per layer). There is no runtime palette |
| * buffer to bind or size. Palette selection and per-run custom |
| * palette overrides are set on the encoder via |
| * hb_gpu_paint_set_palette() / hb_gpu_paint_set_custom_palette_color(); |
| * changing either invalidates previously encoded blobs and requires |
| * re-encoding. The is_foreground flag still routes through the |
| * shader's foreground parameter so a dark-mode toggle does not |
| * require re-encoding. |
| * |
| * --- Fragment-shader contract ------------------------------------ |
| * |
| * Per pixel: |
| * acc = vec4 (0) -- transparent |
| * stack = array<vec4, HB_GPU_PAINT_GROUP_DEPTH> |
| * sp = 0 |
| * |
| * for op in ops: |
| * LAYER_SOLID: |
| * cov = hb_gpu_draw (texcoord, clip_glyph_offset) |
| * col = is_foreground ? v_foreground : rgba_q15 / 32767 |
| * acc = src_over (col * cov, acc) |
| * LAYER_GRADIENT: |
| * cov = hb_gpu_draw (texcoord, clip_glyph_offset) |
| * col = sample_gradient (texcoord, grad_offset, subtype) |
| * acc = src_over (col * cov, acc) |
| * PUSH_GROUP: |
| * stack[sp++] = acc |
| * acc = vec4 (0) |
| * POP_GROUP: |
| * acc = composite (stack[--sp], acc, composite_mode) |
| * |
| * write acc to gl_FragColor |
| * |
| * HB_GPU_PAINT_GROUP_DEPTH is compile-time fixed at 4. Encoder |
| * validates nesting depth and fails encode if a paint tree exceeds |
| * it. Typical COLRv1 emoji nest 0..2 groups deep. |
| * |
| * --- Encoder state ---------------------------------------------- |
| * |
| * hb_gpu_paint_t accumulates, during hb_font_paint_glyph_or_fail: |
| * |
| * - transform stack (visited push/pop_transform) |
| * - current clip-glyph snapshot captured on push_clip_glyph: |
| * (font, codepoint, transform_at_push) |
| * consumed by the next color / gradient callback to emit a |
| * LAYER_SOLID or LAYER_GRADIENT op |
| * - flat op buffer |
| * - flat sub-payload buffer (bytes; offsets recorded as ops emit) |
| * - scratch hb_gpu_draw_t used to rasterize each clip-glyph |
| * outline under its baked transform |
| * - scratch int16 vector for gradient encoding |
| * - current group depth counter (for validation against |
| * HB_GPU_PAINT_GROUP_DEPTH) |
| * - recycled output blob |
| * |
| * To preserve palette indices across harfbuzz's paint pipeline |
| * (which resolves them to hb_color_t before the color / color-stop |
| * callbacks fire), the encoder registers a custom_palette_color |
| * callback that returns a sentinel hb_color_t with the palette |
| * index embedded. Downstream color / color-stop callbacks decode |
| * the sentinel to recover the index. Foreground colors are |
| * signalled independently by the is_foreground flag that harfbuzz |
| * supplies, so the sentinel never collides with a genuine |
| * foreground request. |
| * |
| * On hb_gpu_paint_encode: |
| * - emit header (num_ops, extents, ops_offset) |
| * - emit op stream |
| * - append accumulated sub-payload buffer |
| * - patch any ops that held byte offsets to convert to texel |
| * offsets |
| * - return blob |
| * |
| * --- Out of scope for v1 ---------------------------------------- |
| * |
| * - Clip-rectangle paints (push_clip_rectangle / pop_clip). |
| * COLRv1 allows them but they are rare; v1 emits nothing for |
| * them and the caller loses clipping precision. v2 adds a |
| * RECT_CLIP op with four fixed-point extents. |
| * - Paint images (hb_paint_funcs_set_image_func). Always |
| * returns unhandled. Images are better served by a separate |
| * bitmap path. |
| */ |
| |
| struct hb_gpu_paint_t |
| { |
| hb_object_header_t header; |
| |
| /* Persistent configuration (survives hb_gpu_paint_clear). */ |
| unsigned palette = 0; |
| hb_map_t *custom_palette = nullptr; |
| |
| /* Current effective affine (stack is grown/shrunk by |
| * push_transform / pop_transform callbacks). */ |
| hb_transform_t<float> cur_transform = {1, 0, 0, 1, 0, 0}; |
| hb_vector_t<hb_transform_t<float>> transform_stack; |
| |
| /* Font scale (set by hb_gpu_paint_glyph()). */ |
| int x_scale = 0; |
| int y_scale = 0; |
| |
| /* Accumulator state (cleared by hb_gpu_paint_clear). */ |
| |
| /* Flat int16 op stream. Each op is a sequence of i16 words |
| * whose total length is determined by the op type (see the |
| * design notes at the top of this file). */ |
| hb_vector_t<int16_t> ops; |
| unsigned num_ops = 0; |
| |
| /* Clip-glyph Slug sub-blobs collected during paint walk. |
| * Referenced by sub_blob_index recorded inside ops; |
| * hb_gpu_paint_encode() concatenates them after the op stream and |
| * patches the recorded indices into texel offsets. */ |
| hb_vector_t<hb_blob_t *> sub_blobs; |
| |
| /* Nesting depth of push_group / pop_group. We bail (set |
| * `unsupported`) if it exceeds HB_GPU_PAINT_MAX_GROUP_DEPTH, |
| * which matches HB_GPU_PAINT_GROUP_DEPTH in the fragment shader. */ |
| unsigned group_depth = 0; |
| |
| /* Stack of pending clips. Each color/gradient op consumes the |
| * current state of this stack: the layer is rendered where ALL |
| * stacked clips are opaque (intersection). Capped at depth |
| * HB_GPU_PAINT_MAX_CLIP_DEPTH; deeper pushes set `unsupported`. |
| * The transform is the one current at push_clip_glyph time -- |
| * the clip outline is defined in that coord space, a subsequent |
| * gradient params callback may run under deeper transforms which |
| * we use for the gradient but not the clip outline. */ |
| struct pending_clip_t |
| { |
| hb_codepoint_t glyph; /* HB_CODEPOINT_INVALID for path clips */ |
| hb_font_t *font; /* borrowed; nullptr for path clips */ |
| hb_transform_t<float> transform; |
| /* Path clips are encoded into a sub-blob at push_clip_path_end |
| * time; glyph clips are encoded lazily on first consuming layer |
| * and cached. sub_blob_index is -1 until the encode happens, |
| * then holds the index into c->sub_blobs and ext_* hold the |
| * design-unit extents. */ |
| int sub_blob_index; |
| int ext_x0, ext_y0, ext_x1, ext_y1; |
| }; |
| pending_clip_t clip_stack[3]; |
| unsigned clip_depth = 0; |
| |
| /* Carries the cur_transform captured at push_clip_path_start |
| * through to push_clip_path_end, where the encoded clip is |
| * committed to clip_stack. */ |
| hb_transform_t<float> pending_clip_path_transform = {}; |
| bool pending_clip_path = false; |
| |
| /* Set when the paint walk emits v1-only callbacks we do not yet |
| * support. hb_gpu_paint_encode() returns NULL in that case. */ |
| bool unsupported = false; |
| |
| /* Extents in font design units, accumulated across layers. */ |
| int ext_min_x = 0x7fffffff; |
| int ext_min_y = 0x7fffffff; |
| int ext_max_x = -0x7fffffff; |
| int ext_max_y = -0x7fffffff; |
| |
| /* Scratch: used to rasterize each clip-glyph outline. */ |
| hb_gpu_draw_t *scratch_draw = nullptr; |
| |
| /* Scratch: color stops fetched per gradient callback. */ |
| hb_vector_t<hb_color_stop_t> color_stops_scratch; |
| |
| /* Recycled output blob. */ |
| hb_blob_t *recycled_blob = nullptr; |
| |
| bool fetch_color_stops (hb_color_line_t *color_line) |
| { |
| unsigned count = hb_color_line_get_color_stops (color_line, 0, nullptr, nullptr); |
| if (unlikely (!count || !color_stops_scratch.resize (count))) |
| { |
| color_stops_scratch.resize (0); |
| return false; |
| } |
| hb_color_line_get_color_stops (color_line, 0, &count, color_stops_scratch.arrayZ); |
| return true; |
| } |
| }; |
| |
| |
| #endif /* HB_GPU_PAINT_HH */ |