blob: ee8d9f6c18a45adbc157024ad9e13a92c3482963 [file]
/*
* 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
*/
#include "hb.hh"
#include "hb-gpu.h"
#include "hb-gpu-paint.hh"
#include "hb-draw.hh"
#include "hb-machinery.hh"
#include "hb-paint.hh"
/* ---- Paint callbacks ---- */
/* Op-type tags. Must match the layout documented in
* hb-gpu-paint.hh. */
enum {
HB_GPU_PAINT_OP_LAYER_SOLID = 0,
HB_GPU_PAINT_OP_LAYER_GRADIENT = 1,
HB_GPU_PAINT_OP_PUSH_GROUP = 2,
HB_GPU_PAINT_OP_POP_GROUP = 3,
};
/* Encoder limits. Hitting any of these flips paint->unsupported and
* makes hb_gpu_paint_encode() return NULL rather than emit a blob
* that would render incorrectly. */
#define HB_GPU_PAINT_MAX_OPS 0x7fff /* num_ops is i16 in the blob header */
#define HB_GPU_PAINT_MAX_GROUP_DEPTH 4 /* matches HB_GPU_PAINT_GROUP_DEPTH in fragment shader */
#define HB_GPU_PAINT_MAX_CLIP_DEPTH 3 /* shader can intersect up to 3 clip outlines per layer */
#define HB_GPU_PAINT_MAX_SUB_BLOBS 1024 /* bound clip-glyph rasterizations per paint walk */
/* Layer-op flag bits (LAYER_SOLID texel 0 .g, LAYER_GRADIENT
* texel 0 .g where bits 0-7 hold the gradient subtype). */
#define HB_GPU_PAINT_FLAG_IS_FOREGROUND 0x0001
#define HB_GPU_PAINT_FLAG_HAS_CLIP2 0x0100
#define HB_GPU_PAINT_FLAG_HAS_CLIP3 0x0200
static hb_bool_t
hb_gpu_paint_custom_palette_color (hb_paint_funcs_t *funcs HB_UNUSED,
void *paint_data,
unsigned color_index,
hb_color_t *color,
void *user_data HB_UNUSED)
{
hb_gpu_paint_t *c = (hb_gpu_paint_t *) paint_data;
if (likely (c->custom_palette && hb_map_has (c->custom_palette, color_index)))
{
*color = hb_map_get (c->custom_palette, color_index);
return true;
}
return false;
}
static void
hb_gpu_paint_push_clip_glyph (hb_paint_funcs_t *funcs HB_UNUSED,
void *paint_data,
hb_codepoint_t glyph,
hb_font_t *font,
void *user_data HB_UNUSED)
{
hb_gpu_paint_t *c = (hb_gpu_paint_t *) paint_data;
if (unlikely (c->clip_depth >= HB_GPU_PAINT_MAX_CLIP_DEPTH))
{
c->unsupported = true;
c->clip_depth++;
return;
}
c->clip_stack[c->clip_depth] = { glyph, font, c->cur_transform, -1, 0, 0, 0, 0 };
c->clip_depth++;
}
static void
hb_gpu_paint_pop_clip (hb_paint_funcs_t *funcs HB_UNUSED,
void *paint_data,
void *user_data HB_UNUSED)
{
hb_gpu_paint_t *c = (hb_gpu_paint_t *) paint_data;
if (likely (c->clip_depth > 0)) c->clip_depth--;
}
/* Arbitrary-path clip: hand the caller the same hb_gpu_draw
* funcs the encoder uses for glyph outlines, with scratch_draw
* as the accumulator. push_clip_path_end encodes the captured
* geometry into a sub-blob and commits it to clip_stack just
* like a glyph clip. No font ref needed — the sub-blob is a
* self-contained slug bundle. */
static hb_draw_funcs_t *
hb_gpu_paint_push_clip_path_start (hb_paint_funcs_t *funcs HB_UNUSED,
void *paint_data,
void **draw_data,
void *user_data HB_UNUSED)
{
hb_gpu_paint_t *c = (hb_gpu_paint_t *) paint_data;
if (unlikely (c->clip_depth >= HB_GPU_PAINT_MAX_CLIP_DEPTH))
{
c->unsupported = true;
*draw_data = nullptr;
return nullptr;
}
if (unlikely (!c->scratch_draw))
{
c->scratch_draw = hb_gpu_draw_create_or_fail ();
if (unlikely (!c->scratch_draw))
{
c->unsupported = true;
*draw_data = nullptr;
return nullptr;
}
}
hb_gpu_draw_clear (c->scratch_draw);
hb_gpu_draw_set_scale (c->scratch_draw, c->x_scale, c->y_scale);
c->pending_clip_path_transform = c->cur_transform;
c->pending_clip_path = true;
*draw_data = c->scratch_draw;
return hb_gpu_draw_get_funcs (c->scratch_draw);
}
static void
hb_gpu_paint_push_clip_path_end (hb_paint_funcs_t *funcs HB_UNUSED,
void *paint_data,
void *user_data HB_UNUSED)
{
hb_gpu_paint_t *c = (hb_gpu_paint_t *) paint_data;
if (unlikely (!c->pending_clip_path))
return;
c->pending_clip_path = false;
hb_glyph_extents_t ext;
hb_blob_t *blob = hb_gpu_draw_encode (c->scratch_draw, &ext);
if (unlikely (!blob || !c->sub_blobs.push_or_fail (blob)))
{
hb_blob_destroy (blob);
c->unsupported = true;
return;
}
int idx = (int) (c->sub_blobs.length - 1);
int x0 = ext.x_bearing;
int x1 = (int) ((int64_t) ext.x_bearing + ext.width);
int y0 = ext.y_bearing;
int y1 = (int) ((int64_t) ext.y_bearing + ext.height);
c->clip_stack[c->clip_depth] = {
HB_CODEPOINT_INVALID, nullptr,
c->pending_clip_path_transform,
idx,
hb_min (x0, x1), hb_min (y0, y1),
hb_max (x0, x1), hb_max (y0, y1),
};
c->clip_depth++;
}
static void
hb_gpu_paint_push_transform (hb_paint_funcs_t *funcs HB_UNUSED,
void *paint_data,
float xx, float yx,
float xy, float yy,
float dx, float dy,
void *user_data HB_UNUSED)
{
hb_gpu_paint_t *c = (hb_gpu_paint_t *) paint_data;
if (unlikely (!c->transform_stack.push_or_fail (c->cur_transform)))
{
c->unsupported = true;
return;
}
hb_transform_t<float> t (xx, yx, xy, yy, dx, dy);
c->cur_transform.multiply (t); /* cur = cur * t */
}
static void
hb_gpu_paint_pop_transform (hb_paint_funcs_t *funcs HB_UNUSED,
void *paint_data,
void *user_data HB_UNUSED)
{
hb_gpu_paint_t *c = (hb_gpu_paint_t *) paint_data;
if (unlikely (!c->transform_stack.length))
return;
c->cur_transform = c->transform_stack.arrayZ[c->transform_stack.length - 1];
c->transform_stack.shrink (c->transform_stack.length - 1);
}
/* Emit a 1-texel control op. Used for PUSH_GROUP / POP_GROUP. */
static void
emit_control_op (hb_gpu_paint_t *c, int16_t op_type, int16_t aux)
{
if (unlikely (c->num_ops >= HB_GPU_PAINT_MAX_OPS))
{
c->unsupported = true;
return;
}
if (unlikely (!c->ops.resize (c->ops.length + 4)))
{
c->unsupported = true;
return;
}
int16_t *o = &c->ops.arrayZ[c->ops.length - 4];
o[0] = op_type;
o[1] = aux;
o[2] = 0;
o[3] = 0;
c->num_ops++;
}
static void
hb_gpu_paint_push_group (hb_paint_funcs_t *funcs HB_UNUSED,
void *paint_data,
void *user_data HB_UNUSED)
{
hb_gpu_paint_t *c = (hb_gpu_paint_t *) paint_data;
c->group_depth++;
if (unlikely (c->group_depth > HB_GPU_PAINT_MAX_GROUP_DEPTH))
{
c->unsupported = true;
return;
}
emit_control_op (c, HB_GPU_PAINT_OP_PUSH_GROUP, 0);
}
static void
hb_gpu_paint_pop_group (hb_paint_funcs_t *funcs HB_UNUSED,
void *paint_data,
hb_paint_composite_mode_t mode,
void *user_data HB_UNUSED)
{
hb_gpu_paint_t *c = (hb_gpu_paint_t *) paint_data;
bool was_in_range = c->group_depth <= HB_GPU_PAINT_MAX_GROUP_DEPTH;
if (likely (c->group_depth > 0)) c->group_depth--;
if (was_in_range)
emit_control_op (c, HB_GPU_PAINT_OP_POP_GROUP, (int16_t) mode);
}
/* Quantize unsigned byte 0..255 to signed Q15 0..32767. */
static inline int16_t
color_to_q15 (unsigned byte_value)
{
return (int16_t) ((byte_value * 32767u + 127u) / 255u);
}
static inline void
color_to_q15_rgba (hb_color_t color, int16_t out[4])
{
out[0] = color_to_q15 (hb_color_get_red (color));
out[1] = color_to_q15 (hb_color_get_green (color));
out[2] = color_to_q15 (hb_color_get_blue (color));
out[3] = color_to_q15 (hb_color_get_alpha (color));
}
static inline int16_t
clamp_i16 (float v)
{
if (v <= -32768.f) return -32768;
if (v >= 32767.f) return 32767;
return (int16_t) v;
}
/* Compute the 2x2 inverse of the transform's linear part, quantize
* each entry to i16 Q10 (multiplier 1024) and write row-major into
* @out: (m00, m01, m10, m11) such that applying it gives
* (M^-1 * v).x = m00 * v.x + m01 * v.y
* (M^-1 * v).y = m10 * v.x + m11 * v.y
* Range [-32, 32), precision ~0.001 -- covers typical COLRv1
* PaintTransform values with headroom. Callers who need to skip
* transforms for degenerate cases can check for is_identity before
* doing the extra work in-shader, but we always emit so the format
* stays uniform. */
static inline void
linv_q10 (const hb_transform_t<float> &t, int16_t out[4])
{
float det = t.xx * t.yy - t.xy * t.yx;
float inv = fabsf (det) > 1e-12f ? 1.0f / det : 0.0f;
out[0] = clamp_i16 ( t.yy * inv * 1024.f);
out[1] = clamp_i16 (-t.xy * inv * 1024.f);
out[2] = clamp_i16 (-t.yx * inv * 1024.f);
out[3] = clamp_i16 ( t.xx * inv * 1024.f);
}
/* Transforming pen: applies an affine transform to each outline
* point before forwarding to a downstream hb_draw_funcs_t. The
* downstream's inline draw-state bookkeeping runs against a
* separate state (down_st) so transformed-space updates don't
* pollute the caller's pre-transform state. */
struct hb_gpu_paint_pen_t
{
hb_transform_t<float> transform;
hb_draw_funcs_t *dfuncs;
void *data;
hb_draw_state_t down_st;
};
static void
hb_gpu_paint_pen_move_to (hb_draw_funcs_t *dfuncs HB_UNUSED,
void *data, hb_draw_state_t *st HB_UNUSED,
float to_x, float to_y,
void *user_data HB_UNUSED)
{
auto *c = (hb_gpu_paint_pen_t *) data;
c->transform.transform_point (to_x, to_y);
c->dfuncs->move_to (c->data, c->down_st, to_x, to_y);
}
static void
hb_gpu_paint_pen_line_to (hb_draw_funcs_t *dfuncs HB_UNUSED,
void *data, hb_draw_state_t *st HB_UNUSED,
float to_x, float to_y,
void *user_data HB_UNUSED)
{
auto *c = (hb_gpu_paint_pen_t *) data;
c->transform.transform_point (to_x, to_y);
c->dfuncs->line_to (c->data, c->down_st, to_x, to_y);
}
static void
hb_gpu_paint_pen_quadratic_to (hb_draw_funcs_t *dfuncs HB_UNUSED,
void *data, hb_draw_state_t *st HB_UNUSED,
float c_x, float c_y,
float to_x, float to_y,
void *user_data HB_UNUSED)
{
auto *c = (hb_gpu_paint_pen_t *) data;
c->transform.transform_point (c_x, c_y);
c->transform.transform_point (to_x, to_y);
c->dfuncs->quadratic_to (c->data, c->down_st, c_x, c_y, to_x, to_y);
}
static void
hb_gpu_paint_pen_cubic_to (hb_draw_funcs_t *dfuncs HB_UNUSED,
void *data, hb_draw_state_t *st HB_UNUSED,
float c1_x, float c1_y,
float c2_x, float c2_y,
float to_x, float to_y,
void *user_data HB_UNUSED)
{
auto *c = (hb_gpu_paint_pen_t *) data;
c->transform.transform_point (c1_x, c1_y);
c->transform.transform_point (c2_x, c2_y);
c->transform.transform_point (to_x, to_y);
c->dfuncs->cubic_to (c->data, c->down_st, c1_x, c1_y, c2_x, c2_y, to_x, to_y);
}
static void
hb_gpu_paint_pen_close_path (hb_draw_funcs_t *dfuncs HB_UNUSED,
void *data, hb_draw_state_t *st HB_UNUSED,
void *user_data HB_UNUSED)
{
auto *c = (hb_gpu_paint_pen_t *) data;
c->dfuncs->close_path (c->data, c->down_st);
}
static inline void free_static_gpu_paint_pen_funcs ();
static struct hb_gpu_paint_pen_funcs_lazy_loader_t
: hb_draw_funcs_lazy_loader_t<hb_gpu_paint_pen_funcs_lazy_loader_t>
{
static hb_draw_funcs_t *create ()
{
hb_draw_funcs_t *funcs = hb_draw_funcs_create ();
hb_draw_funcs_set_move_to_func (funcs, hb_gpu_paint_pen_move_to, nullptr, nullptr);
hb_draw_funcs_set_line_to_func (funcs, hb_gpu_paint_pen_line_to, nullptr, nullptr);
hb_draw_funcs_set_quadratic_to_func (funcs, hb_gpu_paint_pen_quadratic_to, nullptr, nullptr);
hb_draw_funcs_set_cubic_to_func (funcs, hb_gpu_paint_pen_cubic_to, nullptr, nullptr);
hb_draw_funcs_set_close_path_func (funcs, hb_gpu_paint_pen_close_path, nullptr, nullptr);
hb_draw_funcs_make_immutable (funcs);
hb_atexit (free_static_gpu_paint_pen_funcs);
return funcs;
}
} static_gpu_paint_pen_funcs;
static inline void
free_static_gpu_paint_pen_funcs ()
{
static_gpu_paint_pen_funcs.free_instance ();
}
/* Rasterize @clip's glyph outline into a new sub-blob and
* accumulate its extents. Returns the sub_blob index (>= 0) on
* success, or -1 if the outline is empty / the layer should be
* skipped. Sets c->unsupported on hard failure. On the first
* successful glyph-clip encode, memoizes the sub-blob index and
* extents back into @clip; subsequent calls hit the cache fast
* path. This matters for adversarial COLR trees that emit many
* paint layers under the same clip stack -- without memoization
* each layer re-rasterizes every clip glyph. */
static int
emit_clip_sub_blob (hb_gpu_paint_t *c,
hb_gpu_paint_t::pending_clip_t &clip)
{
/* Path clips were already encoded into sub_blobs at
* push_clip_path_end time; glyph clips are cached here on first
* use. Either way: just accumulate extents and hand back the
* recorded index. */
if (clip.sub_blob_index >= 0)
{
c->ext_min_x = hb_min (c->ext_min_x, clip.ext_x0);
c->ext_max_x = hb_max (c->ext_max_x, clip.ext_x1);
c->ext_min_y = hb_min (c->ext_min_y, clip.ext_y0);
c->ext_max_y = hb_max (c->ext_max_y, clip.ext_y1);
return clip.sub_blob_index;
}
/* Adversarial COLR trees can drive thousands of fresh
* push_clip_glyph / paint_color cycles within a single paint
* walk, each demanding a fresh sub-blob. Bound the work. */
if (unlikely (c->sub_blobs.length >= HB_GPU_PAINT_MAX_SUB_BLOBS))
{
c->unsupported = true;
return -1;
}
if (unlikely (!c->scratch_draw))
{
c->scratch_draw = hb_gpu_draw_create_or_fail ();
if (unlikely (!c->scratch_draw))
{
c->unsupported = true;
return -1;
}
}
hb_gpu_draw_clear (c->scratch_draw);
bool ok;
if (clip.transform.is_identity ())
{
/* Fast path: feed the glyph outline straight into the draw
* encoder with no adapter. */
ok = hb_gpu_draw_glyph_or_fail (c->scratch_draw, clip.font, clip.glyph);
}
else
{
/* Transform each outline point by the transform that was
* current at push_clip_glyph time -- NOT the innermost
* cur_transform, which may have additional PaintTransform
* wrappers from the paint child underneath. */
int x_scale, y_scale;
hb_font_get_scale (clip.font, &x_scale, &y_scale);
hb_gpu_draw_set_scale (c->scratch_draw, x_scale, y_scale);
hb_gpu_paint_pen_t pen = {};
pen.transform = clip.transform;
pen.dfuncs = hb_gpu_draw_get_funcs (c->scratch_draw);
pen.data = c->scratch_draw;
pen.down_st = HB_DRAW_STATE_DEFAULT;
ok = hb_font_draw_glyph_or_fail (clip.font, clip.glyph,
static_gpu_paint_pen_funcs.get_unconst (),
&pen);
/* Close any trailing open path on the downstream state, since
* hb_font_draw_glyph_or_fail only closes via our pen's state. */
pen.dfuncs->close_path (pen.data, pen.down_st);
}
if (!ok)
return -1; /* Clip glyph has no outline -- skip. */
hb_glyph_extents_t ext;
hb_blob_t *blob = hb_gpu_draw_encode (c->scratch_draw, &ext);
if (unlikely (!blob || !c->sub_blobs.push_or_fail (blob)))
{
hb_blob_destroy (blob);
c->unsupported = true;
return -1;
}
/* Accumulate extents: x_bearing/y_bearing are top-left, width
* positive, height negative (growing down). */
int x0 = ext.x_bearing;
int x1 = (int) ((int64_t) ext.x_bearing + ext.width);
int y0 = ext.y_bearing;
int y1 = (int) ((int64_t) ext.y_bearing + ext.height);
c->ext_min_x = hb_min (c->ext_min_x, hb_min (x0, x1));
c->ext_max_x = hb_max (c->ext_max_x, hb_max (x0, x1));
c->ext_min_y = hb_min (c->ext_min_y, hb_min (y0, y1));
c->ext_max_y = hb_max (c->ext_max_y, hb_max (y0, y1));
int idx = (int) (c->sub_blobs.length - 1);
/* Memoize on the slot. The (glyph, font, transform) tuple is
* fixed between push_clip_glyph and pop_clip, and pop_clip's
* subsequent push reinitializes the slot to sub_blob_index = -1,
* so a stale cache cannot leak across pushes. */
clip.sub_blob_index = idx;
clip.ext_x0 = hb_min (x0, x1);
clip.ext_y0 = hb_min (y0, y1);
clip.ext_x1 = hb_max (x0, x1);
clip.ext_y1 = hb_max (y0, y1);
return idx;
}
/* Emit a sub-blob for every clip currently on the stack and return
* their indices in @out (set to -1 for unused slots). Returns
* false if any clip's outline is empty (caller should skip the
* layer entirely) or hb_gpu_draw_encode failed. */
static bool
emit_all_clip_sub_blobs (hb_gpu_paint_t *c, int out[HB_GPU_PAINT_MAX_CLIP_DEPTH])
{
for (unsigned i = 0; i < HB_GPU_PAINT_MAX_CLIP_DEPTH; i++)
out[i] = -1;
for (unsigned i = 0; i < hb_min (c->clip_depth, (unsigned) HB_GPU_PAINT_MAX_CLIP_DEPTH); i++)
{
out[i] = emit_clip_sub_blob (c, c->clip_stack[i]);
if (out[i] < 0)
return false;
}
return true;
}
/* Emit a 3-texel LAYER_GRADIENT op header (excluding the
* gradient-meta texel which the caller fills in). @subtype is
* 0 = linear, 1 = radial, 2 = sweep. */
static int16_t *
emit_gradient_op_header (hb_gpu_paint_t *c, unsigned subtype,
const int clip_idx[HB_GPU_PAINT_MAX_CLIP_DEPTH])
{
if (unlikely (!c->ops.resize (c->ops.length + 12)))
{ c->unsupported = true; return nullptr; }
int16_t *o = &c->ops.arrayZ[c->ops.length - 12];
int16_t flags = (int16_t) subtype;
if (clip_idx[1] >= 0) flags |= HB_GPU_PAINT_FLAG_HAS_CLIP2;
if (clip_idx[2] >= 0) flags |= HB_GPU_PAINT_FLAG_HAS_CLIP3;
o[0] = HB_GPU_PAINT_OP_LAYER_GRADIENT;
o[1] = flags;
o[2] = (int16_t) ((clip_idx[0] >> 16) & 0xffff);
o[3] = (int16_t) (clip_idx[0] & 0xffff);
o[4] = clip_idx[1] >= 0 ? (int16_t) ((clip_idx[1] >> 16) & 0xffff) : 0;
o[5] = clip_idx[1] >= 0 ? (int16_t) (clip_idx[1] & 0xffff) : 0;
o[6] = clip_idx[2] >= 0 ? (int16_t) ((clip_idx[2] >> 16) & 0xffff) : 0;
o[7] = clip_idx[2] >= 0 ? (int16_t) (clip_idx[2] & 0xffff) : 0;
return o + 8; /* texel 2: gradient meta -- caller fills */
}
static void
hb_gpu_paint_emit_solid (hb_gpu_paint_t *c,
hb_bool_t is_foreground,
hb_color_t color)
{
if (unlikely (c->unsupported))
return;
if (unlikely (c->clip_depth == 0))
return;
if (unlikely (c->num_ops >= HB_GPU_PAINT_MAX_OPS))
{
c->unsupported = true;
return;
}
int clip_idx[HB_GPU_PAINT_MAX_CLIP_DEPTH];
if (!emit_all_clip_sub_blobs (c, clip_idx))
return;
/* Emit LAYER_SOLID op: 3 texels (12 int16s).
* texel 0: op_type, flags, clip1_hi, clip1_lo
* texel 1: clip2_hi, clip2_lo, 0, 0 (only meaningful if HAS_CLIP2)
* texel 2: r_q15, g_q15, b_q15, a_q15
* Payloads hold sub_blob indices; hb_gpu_paint_encode() patches
* them into texel offsets at serialize time.
* flags bit 0: use shader foreground uniform instead of the
* baked color (so runtime foreground-color changes still work).
* flags bit 8: a second clip outline is present in texel 1. */
if (unlikely (!c->ops.resize (c->ops.length + 12)))
{
c->unsupported = true;
return;
}
int16_t *o = &c->ops.arrayZ[c->ops.length - 12];
int16_t flags = (int16_t) (is_foreground ? HB_GPU_PAINT_FLAG_IS_FOREGROUND : 0);
if (clip_idx[1] >= 0) flags |= HB_GPU_PAINT_FLAG_HAS_CLIP2;
if (clip_idx[2] >= 0) flags |= HB_GPU_PAINT_FLAG_HAS_CLIP3;
o[0] = HB_GPU_PAINT_OP_LAYER_SOLID;
o[1] = flags;
o[2] = (int16_t) ((clip_idx[0] >> 16) & 0xffff);
o[3] = (int16_t) (clip_idx[0] & 0xffff);
o[4] = clip_idx[1] >= 0 ? (int16_t) ((clip_idx[1] >> 16) & 0xffff) : 0;
o[5] = clip_idx[1] >= 0 ? (int16_t) (clip_idx[1] & 0xffff) : 0;
o[6] = clip_idx[2] >= 0 ? (int16_t) ((clip_idx[2] >> 16) & 0xffff) : 0;
o[7] = clip_idx[2] >= 0 ? (int16_t) (clip_idx[2] & 0xffff) : 0;
color_to_q15_rgba (color, o + 8);
c->num_ops++;
}
/* Append a color line (@count stops) to @grad_data in the encoded
* stop format used by gradient ops: 2 texels per stop.
* texel a: (offset_q15, flags, _, _) with flags bit 0 = is_foreground
* texel b: (r_q15, g_q15, b_q15, a_q15)
* Returns false on allocation failure. */
static bool
append_color_stops (hb_vector_t<int16_t> &grad_data,
const hb_color_stop_t *stops,
unsigned count)
{
unsigned base = grad_data.length;
if (unlikely (!grad_data.resize (base + count * 8)))
return false;
int16_t *p = grad_data.arrayZ + base;
for (unsigned i = 0; i < count; i++)
{
float off = stops[i].offset;
if (off < -1.f) off = -1.f;
else if (off > 1.f) off = 1.f;
p[0] = (int16_t) (off * 32767.f);
p[1] = (int16_t) (stops[i].is_foreground ? 1 : 0);
p[2] = 0;
p[3] = 0;
color_to_q15_rgba (stops[i].color, p + 4);
p += 8;
}
return true;
}
static void
hb_gpu_paint_emit_linear (hb_gpu_paint_t *c,
hb_color_line_t *color_line,
float x0, float y0,
float x1, float y1,
float x2, float y2)
{
if (unlikely (c->unsupported))
return;
if (unlikely (c->clip_depth == 0))
return;
if (unlikely (c->num_ops >= HB_GPU_PAINT_MAX_OPS))
{
c->unsupported = true;
return;
}
/* Resolve stops (triggers custom_palette_color, etc). */
if (unlikely (!c->fetch_color_stops (color_line)))
return;
hb_color_stop_t *stops = c->color_stops_scratch.arrayZ;
unsigned got = c->color_stops_scratch.length;
hb_paint_extend_t extend = hb_color_line_get_extend (color_line);
/* Emit clip sub-blob(s) first. */
int clip_idx[HB_GPU_PAINT_MAX_CLIP_DEPTH];
if (!emit_all_clip_sub_blobs (c, clip_idx)) return;
/* Build gradient params sub-blob.
* texel 0: (p0_rendered_x, p0_rendered_y, d_canonical_x, d_canonical_y)
* i16 font units; d = p1 - p0 stored in untransformed space
* texel 1: L^-1 as 4 i16 Q10 (row-major)
* texels 2..N: color stops (2 texels each)
* The shader computes p_canonical = L^-1 * (renderCoord - p0_rendered)
* and evaluates t = dot(p_canonical, d) / dot(d, d) in
* untransformed space, so affine distortions of the axis are
* handled exactly rather than approximated. */
hb_vector_t<int16_t> grad_data;
if (unlikely (!grad_data.resize (8)))
{ c->unsupported = true; return; }
float lx0, ly0, lx1, ly1;
hb_paint_reduce_linear_anchors (x0, y0, x1, y1, x2, y2, &lx0, &ly0, &lx1, &ly1);
/* Normalize stops to [0,1]; shift axis endpoints to compensate. */
float mn, mx;
hb_paint_normalize_color_line (stops, got, &mn, &mx);
float dx = lx1 - lx0, dy = ly1 - ly0;
float ax0 = lx0 + mn * dx, ay0 = ly0 + mn * dy;
float ax1 = lx0 + mx * dx, ay1 = ly0 + mx * dy;
/* p0 in rendered space; d in untransformed space. */
float p0x_r = ax0, p0y_r = ay0;
c->cur_transform.transform_point (p0x_r, p0y_r);
grad_data[0] = clamp_i16 (p0x_r);
grad_data[1] = clamp_i16 (p0y_r);
grad_data[2] = clamp_i16 (ax1 - ax0);
grad_data[3] = clamp_i16 (ay1 - ay0);
int16_t Linv[4];
linv_q10 (c->cur_transform, Linv);
grad_data[4] = Linv[0];
grad_data[5] = Linv[1];
grad_data[6] = Linv[2];
grad_data[7] = Linv[3];
if (unlikely (!append_color_stops (grad_data, stops, got)))
{ c->unsupported = true; return; }
unsigned grad_bytes = grad_data.length * sizeof (int16_t);
hb_blob_t *grad_blob = hb_blob_create ((const char *) grad_data.arrayZ,
grad_bytes, HB_MEMORY_MODE_DUPLICATE,
nullptr, nullptr);
if (unlikely (!grad_blob || !c->sub_blobs.push_or_fail (grad_blob)))
{
hb_blob_destroy (grad_blob);
c->unsupported = true;
return;
}
unsigned grad_idx = c->sub_blobs.length - 1;
int16_t *meta = emit_gradient_op_header (c, 0 /* linear */, clip_idx);
if (unlikely (!meta)) return;
meta[0] = (int16_t) ((grad_idx >> 16) & 0xffff);
meta[1] = (int16_t) (grad_idx & 0xffff);
meta[2] = (int16_t) extend;
meta[3] = (int16_t) got;
c->num_ops++;
}
static void
hb_gpu_paint_emit_radial (hb_gpu_paint_t *c,
hb_color_line_t *color_line,
float x0, float y0, float r0,
float x1, float y1, float r1)
{
if (unlikely (c->unsupported))
return;
if (unlikely (c->clip_depth == 0))
return;
if (unlikely (c->num_ops >= HB_GPU_PAINT_MAX_OPS))
{
c->unsupported = true;
return;
}
if (unlikely (!c->fetch_color_stops (color_line)))
return;
hb_color_stop_t *stops = c->color_stops_scratch.arrayZ;
unsigned got = c->color_stops_scratch.length;
hb_paint_extend_t extend = hb_color_line_get_extend (color_line);
int clip_idx[HB_GPU_PAINT_MAX_CLIP_DEPTH];
if (!emit_all_clip_sub_blobs (c, clip_idx)) return;
/* Build gradient params sub-blob.
* texel 0: (c0_rendered_x, c0_rendered_y, d_canonical_x, d_canonical_y)
* where d = c1 - c0 in untransformed space
* texel 1: (r0, r1, _, _) in untransformed font units (i16)
* texel 2: L^-1 as 4 i16 Q10 (row-major)
* texels 3..N: color stops (2 texels each)
* Shader computes p_canonical = L^-1 * (renderCoord - c0_rendered)
* then solves |p - t*d|^2 = (r0 + t*(r1-r0))^2 in the
* untransformed frame, so non-uniform scale / shear (which would
* turn the circle into an ellipse in rendered space) is handled
* exactly.
*/
hb_vector_t<int16_t> grad_data;
if (unlikely (!grad_data.resize (12)))
{ c->unsupported = true; return; }
float mn, mx;
hb_paint_normalize_color_line (stops, got, &mn, &mx);
float dx = x1 - x0, dy = y1 - y0, dr = r1 - r0;
float cx0 = x0 + mn * dx, cy0 = y0 + mn * dy, cr0 = r0 + mn * dr;
float cx1 = x0 + mx * dx, cy1 = y0 + mx * dy, cr1 = r0 + mx * dr;
/* c0 in rendered space; d (= c1 - c0) and radii in untransformed. */
float cx0_r = cx0, cy0_r = cy0;
c->cur_transform.transform_point (cx0_r, cy0_r);
grad_data[0] = clamp_i16 (cx0_r);
grad_data[1] = clamp_i16 (cy0_r);
grad_data[2] = clamp_i16 (cx1 - cx0);
grad_data[3] = clamp_i16 (cy1 - cy0);
grad_data[4] = clamp_i16 (cr0);
grad_data[5] = clamp_i16 (cr1);
grad_data[6] = 0;
grad_data[7] = 0;
int16_t Linv[4];
linv_q10 (c->cur_transform, Linv);
grad_data[8] = Linv[0];
grad_data[9] = Linv[1];
grad_data[10] = Linv[2];
grad_data[11] = Linv[3];
if (unlikely (!append_color_stops (grad_data, stops, got)))
{ c->unsupported = true; return; }
unsigned grad_bytes = grad_data.length * sizeof (int16_t);
hb_blob_t *grad_blob = hb_blob_create ((const char *) grad_data.arrayZ,
grad_bytes, HB_MEMORY_MODE_DUPLICATE,
nullptr, nullptr);
if (unlikely (!grad_blob || !c->sub_blobs.push_or_fail (grad_blob)))
{
hb_blob_destroy (grad_blob);
c->unsupported = true;
return;
}
unsigned grad_idx = c->sub_blobs.length - 1;
int16_t *meta = emit_gradient_op_header (c, 1 /* radial */, clip_idx);
if (unlikely (!meta)) return;
meta[0] = (int16_t) ((grad_idx >> 16) & 0xffff);
meta[1] = (int16_t) (grad_idx & 0xffff);
meta[2] = (int16_t) extend;
meta[3] = (int16_t) got;
c->num_ops++;
}
static void
hb_gpu_paint_color (hb_paint_funcs_t *funcs HB_UNUSED,
void *paint_data,
hb_bool_t is_foreground,
hb_color_t color,
void *user_data HB_UNUSED)
{
hb_gpu_paint_emit_solid ((hb_gpu_paint_t *) paint_data, is_foreground, color);
}
static void
hb_gpu_paint_linear_gradient (hb_paint_funcs_t *funcs HB_UNUSED,
void *paint_data,
hb_color_line_t *color_line,
float x0, float y0,
float x1, float y1,
float x2, float y2,
void *user_data HB_UNUSED)
{
hb_gpu_paint_emit_linear ((hb_gpu_paint_t *) paint_data, color_line,
x0, y0, x1, y1, x2, y2);
}
static void
hb_gpu_paint_radial_gradient (hb_paint_funcs_t *funcs HB_UNUSED,
void *paint_data,
hb_color_line_t *color_line,
float x0, float y0, float r0,
float x1, float y1, float r1,
void *user_data HB_UNUSED)
{
hb_gpu_paint_emit_radial ((hb_gpu_paint_t *) paint_data, color_line,
x0, y0, r0, x1, y1, r1);
}
static void
hb_gpu_paint_emit_sweep (hb_gpu_paint_t *c,
hb_color_line_t *color_line,
float cx, float cy,
float start_angle, float end_angle)
{
if (unlikely (c->unsupported))
return;
if (unlikely (c->clip_depth == 0))
return;
if (unlikely (c->num_ops >= HB_GPU_PAINT_MAX_OPS))
{
c->unsupported = true;
return;
}
unsigned count = hb_color_line_get_color_stops (color_line, 0, nullptr, nullptr);
if (unlikely (!count))
return;
hb_color_stop_t stack_stops[16];
hb_color_stop_t *stops = stack_stops;
hb_color_stop_t *heap_stops = nullptr;
if (count > 16)
{
heap_stops = (hb_color_stop_t *) hb_malloc (count * sizeof (hb_color_stop_t));
if (unlikely (!heap_stops)) { c->unsupported = true; return; }
stops = heap_stops;
}
unsigned got = count;
hb_color_line_get_color_stops (color_line, 0, &got, stops);
hb_paint_extend_t extend = hb_color_line_get_extend (color_line);
int clip_idx[HB_GPU_PAINT_MAX_CLIP_DEPTH];
if (!emit_all_clip_sub_blobs (c, clip_idx)) { hb_free (heap_stops); return; }
/* Build gradient params sub-blob.
* texel 0: (center_rendered_x, center_rendered_y,
* start_q14, end_q14)
* angles in radians, stored as Q14 fractions of pi; start/end
* stay in untransformed space (no rotation baked in).
* texel 1: L^-1 as 4 i16 Q10 (row-major)
* texels 2..N: color stops (2 texels each)
* Shader does p = L^-1 * (renderCoord - center_rendered) and
* atan2 on p, so non-uniform scale / shear angular distortion is
* handled exactly.
*/
hb_vector_t<int16_t> grad_data;
if (unlikely (!grad_data.resize (8)))
{ c->unsupported = true; hb_free (heap_stops); return; }
float mn, mx;
hb_paint_normalize_color_line (stops, got, &mn, &mx);
float a_span = end_angle - start_angle;
float a_lo = start_angle + mn * a_span;
float a_hi = start_angle + mx * a_span;
auto rad_to_q14_pi = [] (float rad) -> int16_t {
float v = rad * (float) (1.0 / M_PI); /* fraction of pi */
if (v <= -2.f) return -32768;
if (v >= 2.f) return 32767;
return (int16_t) (v * 16384.f);
};
/* center in rendered space; angles stay canonical. */
float tcx = cx, tcy = cy;
c->cur_transform.transform_point (tcx, tcy);
grad_data[0] = clamp_i16 (tcx);
grad_data[1] = clamp_i16 (tcy);
grad_data[2] = rad_to_q14_pi (a_lo);
grad_data[3] = rad_to_q14_pi (a_hi);
int16_t Linv[4];
linv_q10 (c->cur_transform, Linv);
grad_data[4] = Linv[0];
grad_data[5] = Linv[1];
grad_data[6] = Linv[2];
grad_data[7] = Linv[3];
if (unlikely (!append_color_stops (grad_data, stops, got)))
{ c->unsupported = true; hb_free (heap_stops); return; }
hb_free (heap_stops);
unsigned grad_bytes = grad_data.length * sizeof (int16_t);
hb_blob_t *grad_blob = hb_blob_create ((const char *) grad_data.arrayZ,
grad_bytes, HB_MEMORY_MODE_DUPLICATE,
nullptr, nullptr);
if (unlikely (!grad_blob || !c->sub_blobs.push_or_fail (grad_blob)))
{
hb_blob_destroy (grad_blob);
c->unsupported = true;
return;
}
unsigned grad_idx = c->sub_blobs.length - 1;
int16_t *meta = emit_gradient_op_header (c, 2 /* sweep */, clip_idx);
if (unlikely (!meta)) return;
meta[0] = (int16_t) ((grad_idx >> 16) & 0xffff);
meta[1] = (int16_t) (grad_idx & 0xffff);
meta[2] = (int16_t) extend;
meta[3] = (int16_t) got;
c->num_ops++;
}
static void
hb_gpu_paint_sweep_gradient (hb_paint_funcs_t *funcs HB_UNUSED,
void *paint_data,
hb_color_line_t *color_line,
float cx, float cy,
float start_angle,
float end_angle,
void *user_data HB_UNUSED)
{
hb_gpu_paint_emit_sweep ((hb_gpu_paint_t *) paint_data, color_line,
cx, cy, start_angle, end_angle);
}
/* PaintImage isn't representable by our slug+gradient encoder; mark
* unsupported so the caller learns the glyph won't render rather
* than getting a partial blob. PaintRectangleClip is intentionally
* left to the default no-op: the encoder simply ignores the
* rectangle (output is bigger than it should be, but renders). */
static hb_bool_t
hb_gpu_paint_image (hb_paint_funcs_t *funcs HB_UNUSED,
void *paint_data,
hb_blob_t *image HB_UNUSED,
unsigned int width HB_UNUSED,
unsigned int height HB_UNUSED,
hb_tag_t format HB_UNUSED,
float slant HB_UNUSED,
hb_glyph_extents_t *extents HB_UNUSED,
void *user_data HB_UNUSED)
{
((hb_gpu_paint_t *) paint_data)->unsupported = true;
return false;
}
static inline void free_static_gpu_paint_funcs ();
static struct hb_gpu_paint_funcs_lazy_loader_t
: hb_paint_funcs_lazy_loader_t<hb_gpu_paint_funcs_lazy_loader_t>
{
static hb_paint_funcs_t *create ()
{
hb_paint_funcs_t *funcs = hb_paint_funcs_create ();
hb_paint_funcs_set_push_transform_func (funcs, hb_gpu_paint_push_transform, nullptr, nullptr);
hb_paint_funcs_set_pop_transform_func (funcs, hb_gpu_paint_pop_transform, nullptr, nullptr);
hb_paint_funcs_set_push_clip_glyph_func (funcs, hb_gpu_paint_push_clip_glyph, nullptr, nullptr);
hb_paint_funcs_set_push_clip_path_start_func (funcs, hb_gpu_paint_push_clip_path_start, nullptr, nullptr);
hb_paint_funcs_set_push_clip_path_end_func (funcs, hb_gpu_paint_push_clip_path_end, nullptr, nullptr);
hb_paint_funcs_set_pop_clip_func (funcs, hb_gpu_paint_pop_clip, nullptr, nullptr);
hb_paint_funcs_set_push_group_func (funcs, hb_gpu_paint_push_group, nullptr, nullptr);
hb_paint_funcs_set_pop_group_func (funcs, hb_gpu_paint_pop_group, nullptr, nullptr);
hb_paint_funcs_set_color_func (funcs, hb_gpu_paint_color, nullptr, nullptr);
hb_paint_funcs_set_linear_gradient_func (funcs, hb_gpu_paint_linear_gradient, nullptr, nullptr);
hb_paint_funcs_set_radial_gradient_func (funcs, hb_gpu_paint_radial_gradient, nullptr, nullptr);
hb_paint_funcs_set_sweep_gradient_func (funcs, hb_gpu_paint_sweep_gradient, nullptr, nullptr);
hb_paint_funcs_set_custom_palette_color_func (funcs, hb_gpu_paint_custom_palette_color, nullptr, nullptr);
/* PaintImage can't be represented by our slug+gradient encoder. */
hb_paint_funcs_set_image_func (funcs, hb_gpu_paint_image, nullptr, nullptr);
hb_paint_funcs_make_immutable (funcs);
hb_atexit (free_static_gpu_paint_funcs);
return funcs;
}
} static_gpu_paint_funcs;
static inline void
free_static_gpu_paint_funcs ()
{
static_gpu_paint_funcs.free_instance ();
}
/* ---- Public API ---- */
/**
* hb_gpu_paint_create_or_fail:
*
* Creates a new GPU color-glyph paint encoder.
*
* Return value: (transfer full):
* A newly allocated #hb_gpu_paint_t, or `NULL` on allocation
* failure.
*
* Since: 14.2.0
**/
hb_gpu_paint_t *
hb_gpu_paint_create_or_fail (void)
{
return hb_object_create<hb_gpu_paint_t> ();
}
/**
* hb_gpu_paint_reference: (skip)
* @paint: a GPU color-glyph paint encoder
*
* Increases the reference count on @paint by one.
*
* Return value: (transfer full):
* The referenced #hb_gpu_paint_t.
*
* Since: 14.2.0
**/
hb_gpu_paint_t *
hb_gpu_paint_reference (hb_gpu_paint_t *paint)
{
return hb_object_reference (paint);
}
/**
* hb_gpu_paint_destroy: (skip)
* @paint: a GPU color-glyph paint encoder
*
* Decreases the reference count on @paint by one. When the
* reference count reaches zero, the encoder is freed.
*
* Since: 14.2.0
**/
void
hb_gpu_paint_destroy (hb_gpu_paint_t *paint)
{
if (!hb_object_should_destroy (paint))
return;
hb_map_destroy (paint->custom_palette);
for (hb_blob_t *b : paint->sub_blobs)
hb_blob_destroy (b);
hb_gpu_draw_destroy (paint->scratch_draw);
hb_blob_destroy (paint->recycled_blob);
hb_object_actually_destroy (paint);
hb_free (paint);
}
/**
* hb_gpu_paint_set_user_data: (skip)
* @paint: a GPU color-glyph paint encoder
* @key: the user-data key
* @data: a pointer to the user data
* @destroy: (nullable): a callback to call when @data is not needed anymore
* @replace: whether to replace an existing data with the same key
*
* Attaches user data to @paint.
*
* Return value: `true` if success, `false` otherwise
*
* Since: 14.2.0
**/
hb_bool_t
hb_gpu_paint_set_user_data (hb_gpu_paint_t *paint,
hb_user_data_key_t *key,
void *data,
hb_destroy_func_t destroy,
hb_bool_t replace)
{
return hb_object_set_user_data (paint, key, data, destroy, replace);
}
/**
* hb_gpu_paint_get_user_data: (skip)
* @paint: a GPU color-glyph paint encoder
* @key: the user-data key
*
* Fetches the user data associated with the specified key.
*
* Return value: (transfer none):
* A pointer to the user data.
*
* Since: 14.2.0
**/
void *
hb_gpu_paint_get_user_data (const hb_gpu_paint_t *paint,
hb_user_data_key_t *key)
{
return hb_object_get_user_data (paint, key);
}
/**
* hb_gpu_paint_get_funcs:
* @paint: a GPU paint context.
*
* Fetches the #hb_paint_funcs_t that feeds paint data into
* @paint. Pass @paint as the @paint_data argument when
* calling the paint functions.
*
* Return value: (transfer none):
* The GPU paint functions
*
* Since: 14.2.0
**/
hb_paint_funcs_t *
hb_gpu_paint_get_funcs (const hb_gpu_paint_t *paint HB_UNUSED)
{
return static_gpu_paint_funcs.get_unconst ();
}
/**
* hb_gpu_paint_set_palette:
* @paint: a GPU color-glyph paint encoder
* @palette: palette index
*
* Selects which font palette is used when paint callbacks look up
* indexed colors. Default is palette 0.
*
* Since: 14.2.0
**/
void
hb_gpu_paint_set_palette (hb_gpu_paint_t *paint,
unsigned palette)
{
paint->palette = palette;
}
/**
* hb_gpu_paint_get_palette:
* @paint: a GPU color-glyph paint encoder
*
* Returns the palette index previously set on @paint, or 0 if none
* was set.
*
* Return value: the palette index.
*
* Since: 14.2.0
**/
unsigned
hb_gpu_paint_get_palette (const hb_gpu_paint_t *paint)
{
return paint->palette;
}
/**
* hb_gpu_paint_clear_custom_palette_colors:
* @paint: a GPU color-glyph paint encoder
*
* Clears all custom palette color overrides previously set on @paint.
*
* Since: 14.2.0
**/
void
hb_gpu_paint_clear_custom_palette_colors (hb_gpu_paint_t *paint)
{
if (paint->custom_palette)
hb_map_clear (paint->custom_palette);
}
/**
* hb_gpu_paint_set_custom_palette_color:
* @paint: a GPU color-glyph paint encoder
* @color_index: palette index to override
* @color: replacement color
*
* Overrides one font palette color entry. Overrides are keyed by
* @color_index and persist on @paint until cleared (or replaced for
* the same index).
*
* Return value: `true` if the override was set; `false` on allocation failure.
*
* Since: 14.2.0
**/
hb_bool_t
hb_gpu_paint_set_custom_palette_color (hb_gpu_paint_t *paint,
unsigned int color_index,
hb_color_t color)
{
if (unlikely (!paint->custom_palette))
{
paint->custom_palette = hb_map_create ();
if (unlikely (!paint->custom_palette))
return false;
}
hb_map_set (paint->custom_palette, color_index, color);
return hb_map_allocation_successful (paint->custom_palette);
}
/**
* hb_gpu_paint_set_scale:
* @paint: a GPU color-glyph paint encoder
* @x_scale: horizontal scale (typically from hb_font_get_scale())
* @y_scale: vertical scale
*
* Sets the font scale used to dimension clip-glyph slug outlines
* inside the encoded blob. Called automatically by
* hb_gpu_paint_glyph().
*
* Since: 14.2.0
**/
void
hb_gpu_paint_set_scale (hb_gpu_paint_t *paint,
int x_scale,
int y_scale)
{
paint->x_scale = x_scale;
paint->y_scale = y_scale;
}
/**
* hb_gpu_paint_get_scale:
* @paint: a GPU color-glyph paint encoder
* @x_scale: (out): horizontal scale
* @y_scale: (out): vertical scale
*
* Fetches the font scale previously set on @paint.
*
* Since: 14.2.0
**/
void
hb_gpu_paint_get_scale (const hb_gpu_paint_t *paint,
int *x_scale,
int *y_scale)
{
if (x_scale) *x_scale = paint->x_scale;
if (y_scale) *y_scale = paint->y_scale;
}
/**
* hb_gpu_paint_glyph_or_fail:
* @paint: a GPU color-glyph paint encoder
* @font: font to paint from
* @glyph: glyph ID to paint
*
* Convenience to feed @glyph's paint tree into the encoder.
* Equivalent to:
*
* |[<!-- language="plain" -->
* hb_gpu_paint_set_scale (paint, x_scale, y_scale);
* hb_font_paint_glyph_or_fail (font, glyph,
* hb_gpu_paint_get_funcs (paint), paint,
* palette, HB_COLOR (0, 0, 0, 0xff));
* ]|
*
* Encoder-level limitations (unsupported paint ops, group-stack
* overflow) do NOT fail here -- they surface later from
* hb_gpu_paint_encode() which returns `NULL`.
*
* Return value: `true` if the font had paint data for @glyph,
* `false` otherwise.
*
* Since: 14.2.0
**/
hb_bool_t
hb_gpu_paint_glyph_or_fail (hb_gpu_paint_t *paint,
hb_font_t *font,
hb_codepoint_t glyph)
{
int x_scale, y_scale;
hb_font_get_scale (font, &x_scale, &y_scale);
hb_gpu_paint_set_scale (paint, x_scale, y_scale);
return hb_font_paint_glyph_or_fail (font, glyph,
hb_gpu_paint_get_funcs (paint), paint,
paint->palette,
/* Foreground RGB is not read from
* the encoded blob -- the
* is_foreground flag routes to the
* shader's foreground uniform.
* Alpha must be opaque so the
* baked color carries only the
* paint-tree alpha (which the
* shader applies on top of the
* uniform). */
HB_COLOR (0, 0, 0, 0xff));
}
/**
* hb_gpu_paint_glyph:
* @paint: a GPU color-glyph paint encoder
* @font: font to paint from
* @glyph: glyph ID to paint
*
* Feeds @glyph into the encoder. Unlike
* hb_gpu_paint_glyph_or_fail(), non-color glyphs are handled
* transparently via a synthesized foreground-colored layer,
* so any glyph with an outline produces output.
*
* Since: 14.2.0
**/
void
hb_gpu_paint_glyph (hb_gpu_paint_t *paint,
hb_font_t *font,
hb_codepoint_t glyph)
{
int x_scale, y_scale;
hb_font_get_scale (font, &x_scale, &y_scale);
hb_gpu_paint_set_scale (paint, x_scale, y_scale);
hb_font_paint_glyph (font, glyph,
hb_gpu_paint_get_funcs (paint), paint,
paint->palette,
/* Foreground RGB is not read from
* the encoded blob -- the
* is_foreground flag routes to the
* shader's foreground uniform.
* Alpha must be opaque so the
* baked color carries only the
* paint-tree alpha (which the
* shader applies on top of the
* uniform). */
HB_COLOR (0, 0, 0, 0xff));
}
/**
* hb_gpu_paint_encode:
* @paint: a GPU color-glyph paint encoder
* @extents: (out): receives the glyph's ink extents in font units
*
* Encodes the accumulated paint state into a GPU-renderable blob
* and writes the glyph's ink extents to @extents.
*
* On return @paint is auto-cleared so it can be reused for the
* next glyph; user configuration (palette, foreground, custom
* palette overrides) is preserved.
*
* Return value: (transfer full):
* A newly allocated blob, or `NULL` if the paint walk hit an
* unsupported feature (see hb_gpu_paint_glyph()'s return value) or
* encoding failed (allocation failure). When the paint accumulated
* no ink (e.g. a space glyph) returns the empty-blob singleton, so
* callers can distinguish "nothing to render" (length 0) from a
* real failure (`NULL`).
*
* Since: 14.2.0
**/
hb_blob_t *
hb_gpu_paint_encode (hb_gpu_paint_t *paint,
hb_glyph_extents_t *extents)
{
HB_SCOPE_GUARD (hb_gpu_paint_clear (paint));
if (unlikely (paint->unsupported))
return nullptr;
/* No ink (e.g. space glyph): return the empty-blob singleton, not
* nullptr. Same convention as hb_gpu_draw_encode(); callers can
* distinguish "nothing to render" (length 0) from "encoder
* failed" (NULL). */
if (paint->num_ops == 0)
return hb_blob_get_empty ();
/* Layout:
* header (3 texels = 24 bytes)
* op stream (ops.length i16 words, multiple of 4)
* sub-payloads (concat of each sub_blob's data, 8-byte
* aligned since draw blobs are RGBA16I)
*/
const unsigned header_texels = 3;
const unsigned texel_bytes = 8;
unsigned ops_texels = paint->ops.length / 4;
unsigned sub_bytes = 0;
for (hb_blob_t *b : paint->sub_blobs)
sub_bytes += hb_blob_get_length (b);
/* Sub-blobs come from the draw encoder which produces 8-byte
* aligned blobs; assert so we notice if that ever changes. */
if (unlikely (sub_bytes % texel_bytes))
return nullptr;
unsigned total_bytes = header_texels * texel_bytes
+ paint->ops.length * 2
+ sub_bytes;
unsigned buf_capacity = 0;
char *replaced_recycled_buf = nullptr;
char *buf_raw = hb_blob_t::recycle_acquire (paint->recycled_blob, total_bytes,
&buf_capacity, &replaced_recycled_buf);
if (unlikely (!buf_raw))
return nullptr;
int16_t *buf = (int16_t *) (void *) buf_raw;
/* Compute each sub_blob's texel offset (relative to blob base). */
unsigned sub_offsets_count = paint->sub_blobs.length;
hb_vector_t<unsigned> sub_offsets;
if (unlikely (!sub_offsets.resize (sub_offsets_count)))
{
hb_blob_t::recycle_abort (buf_raw, paint->recycled_blob);
return nullptr;
}
unsigned cursor = header_texels + ops_texels;
for (unsigned i = 0; i < sub_offsets_count; i++)
{
sub_offsets[i] = cursor;
cursor += hb_blob_get_length (paint->sub_blobs[i]) / texel_bytes;
}
/* Header. */
buf[0] = (int16_t) paint->num_ops;
buf[1] = 0;
buf[2] = 0;
buf[3] = 0;
buf[4] = (int16_t) hb_clamp (paint->ext_min_x, -0x7fff, 0x7fff);
buf[5] = (int16_t) hb_clamp (paint->ext_min_y, -0x7fff, 0x7fff);
buf[6] = (int16_t) hb_clamp (paint->ext_max_x, -0x7fff, 0x7fff);
buf[7] = (int16_t) hb_clamp (paint->ext_max_y, -0x7fff, 0x7fff);
buf[8] = (int16_t) header_texels;
buf[9] = 0;
buf[10] = 0;
buf[11] = 0;
/* Op stream: copy raw, then patch payload slots to texel
* offsets where op_type indicates a sub_blob reference. */
int16_t *ops_out = buf + header_texels * 4;
hb_memcpy (ops_out, paint->ops.arrayZ, paint->ops.length * 2);
unsigned i = 0;
while (i < paint->ops.length)
{
int16_t op_type = ops_out[i];
switch (op_type)
{
case HB_GPU_PAINT_OP_LAYER_SOLID:
case HB_GPU_PAINT_OP_LAYER_GRADIENT:
{
int16_t flags = ops_out[i + 1];
/* Sub-blob slots in i16 offsets within the op:
* clip1: [i+2, i+3] -- always present
* clip2: [i+4, i+5] -- only if HAS_CLIP2
* clip3: [i+6, i+7] -- only if HAS_CLIP3
* grad : [i+8, i+9] (LAYER_GRADIENT only) */
unsigned slots[4] = {
2,
(flags & HB_GPU_PAINT_FLAG_HAS_CLIP2) ? 4u : 0u,
(flags & HB_GPU_PAINT_FLAG_HAS_CLIP3) ? 6u : 0u,
op_type == HB_GPU_PAINT_OP_LAYER_GRADIENT ? 8u : 0u
};
for (unsigned k = 0; k < 4; k++)
{
if (!slots[k]) continue;
unsigned p = i + slots[k];
unsigned idx = ((uint16_t) ops_out[p] << 16)
| (uint16_t) ops_out[p + 1];
if (idx < sub_offsets_count)
{
unsigned off = sub_offsets[idx];
ops_out[p] = (int16_t) ((off >> 16) & 0xffff);
ops_out[p + 1] = (int16_t) (off & 0xffff);
}
}
i += 12; /* 3 texels */
break;
}
case HB_GPU_PAINT_OP_PUSH_GROUP:
case HB_GPU_PAINT_OP_POP_GROUP:
i += 4; /* 1 texel */
break;
default:
hb_blob_t::recycle_abort (buf_raw, paint->recycled_blob);
return nullptr;
}
}
/* Sub-payloads: concat in recorded order. */
char *dst = (char *) buf + header_texels * texel_bytes
+ paint->ops.length * 2;
for (hb_blob_t *b : paint->sub_blobs)
{
unsigned len = hb_blob_get_length (b);
hb_memcpy (dst, hb_blob_get_data (b, nullptr), len);
dst += len;
}
if (extents)
{
extents->x_bearing = paint->ext_min_x;
extents->y_bearing = paint->ext_max_y;
extents->width = (int) ((int64_t) paint->ext_max_x - paint->ext_min_x);
extents->height = (int) ((int64_t) paint->ext_min_y - paint->ext_max_y);
}
hb_blob_t *recycled = paint->recycled_blob;
paint->recycled_blob = nullptr;
return hb_blob_t::recycle_finalize (buf_raw, buf_capacity, total_bytes,
recycled, replaced_recycled_buf);
}
/**
* hb_gpu_paint_clear:
* @paint: a GPU color-glyph paint encoder
*
* Discards accumulated paint state so @paint can be reused for
* another encode. User configuration (font scale) is preserved.
*
* Since: 14.2.0
**/
void
hb_gpu_paint_clear (hb_gpu_paint_t *paint)
{
/* Use reset() not resize(0): if an earlier push_or_fail() set
* the vector's error state (common under failing-malloc),
* resize(0) short-circuits without clearing length. A
* subsequent clear would double-destroy the blob pointers left
* in the array, or keep stale ops across encodes. */
paint->ops.reset ();
paint->num_ops = 0;
for (hb_blob_t *b : paint->sub_blobs)
hb_blob_destroy (b);
paint->sub_blobs.reset ();
paint->clip_depth = 0;
paint->pending_clip_path = false;
paint->unsupported = false;
paint->cur_transform = {1, 0, 0, 1, 0, 0};
paint->transform_stack.reset ();
paint->ext_min_x = 0x7fffffff;
paint->ext_min_y = 0x7fffffff;
paint->ext_max_x = -0x7fffffff;
paint->ext_max_y = -0x7fffffff;
}
/**
* hb_gpu_paint_reset:
* @paint: a GPU color-glyph paint encoder
*
* Resets @paint, discarding accumulated state and user
* configuration.
*
* Since: 14.2.0
**/
void
hb_gpu_paint_reset (hb_gpu_paint_t *paint)
{
paint->x_scale = 0;
paint->y_scale = 0;
paint->palette = 0;
hb_map_destroy (paint->custom_palette);
paint->custom_palette = nullptr;
hb_gpu_paint_clear (paint);
}
/**
* hb_gpu_paint_recycle_blob:
* @paint: a GPU color-glyph paint encoder
* @blob: (transfer full): a blob previously returned by hb_gpu_paint_encode()
*
* Returns a blob to the encoder for potential reuse. The caller
* transfers ownership of @blob.
*
* If @blob came from hb_gpu_paint_encode() its underlying buffer
* will be reused by the next call to hb_gpu_paint_encode(),
* avoiding a malloc / blob allocation per glyph.
*
* Since: 14.2.0
**/
void
hb_gpu_paint_recycle_blob (hb_gpu_paint_t *paint,
hb_blob_t *blob)
{
hb_blob_t::recycle_stash (&paint->recycled_blob, blob);
}
#include "hb-gpu-paint-fragment-glsl.hh"
#include "hb-gpu-paint-fragment-msl.hh"
#include "hb-gpu-paint-fragment-wgsl.hh"
#include "hb-gpu-paint-fragment-hlsl.hh"
/**
* hb_gpu_paint_shader_source:
* @stage: pipeline stage (vertex or fragment)
* @lang: shader language variant
*
* Returns the paint-renderer-specific shader source for the
* specified stage and language. The returned string is static
* and must not be freed.
*
* This source assumes both the shared helpers
* (hb_gpu_shader_source()) and the draw-renderer helpers
* (hb_gpu_draw_shader_source()) are concatenated ahead of it --
* the paint interpreter invokes hb_gpu_draw() to compute
* clip-glyph coverage. Full assembly:
*
* [#version] + hb_gpu_shader_source ()
* + hb_gpu_draw_shader_source ()
* + hb_gpu_paint_shader_source ()
* + caller's main ()
*
* Return value: (transfer none):
* A shader source string, or `NULL` if @stage or @lang is
* unsupported.
*
* Since: 14.2.0
**/
const char *
hb_gpu_paint_shader_source (hb_gpu_shader_stage_t stage,
hb_gpu_shader_lang_t lang)
{
switch (stage) {
case HB_GPU_SHADER_STAGE_FRAGMENT:
switch (lang) {
case HB_GPU_SHADER_LANG_GLSL: return hb_gpu_paint_fragment_glsl;
case HB_GPU_SHADER_LANG_MSL: return hb_gpu_paint_fragment_msl;
case HB_GPU_SHADER_LANG_WGSL: return hb_gpu_paint_fragment_wgsl;
case HB_GPU_SHADER_LANG_HLSL: return hb_gpu_paint_fragment_hlsl;
case HB_GPU_SHADER_LANG_INVALID:
default: return nullptr;
}
case HB_GPU_SHADER_STAGE_VERTEX:
switch (lang) {
case HB_GPU_SHADER_LANG_GLSL:
case HB_GPU_SHADER_LANG_MSL:
case HB_GPU_SHADER_LANG_WGSL:
case HB_GPU_SHADER_LANG_HLSL: return "";
case HB_GPU_SHADER_LANG_INVALID:
default: return nullptr;
}
default:
return nullptr;
}
}