ludic/packages/ludic.render3d/overlay.ludic
Orkuncakilkaya 65c1b9ca5c feat(render3d): HDR calibration; fix the HDR toggle crash and yellow reading as red
r3d_hdr_calibrate(peak, paper, black) feeds the tonemap's and the overlay's HDR10 variants and
the display's HDR metadata; ov_hdr_nits draws a calibration patch at a number of nits.

gpu_caps_probe asks the running Vulkan renderer's instance instead of making and destroying a
second one under Streamline's interposer, which left the next swapchain rebuild calling address 0.
The overlay gets an HDR10 variant, and the tonemap brightens HDR highlights by one factor rather
than per channel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 22:57:18 +03:00

464 lines
21 KiB
Text

# ============================================================================
# overlay.ludic — 2D drawing over the finished frame, in screen pixels with the
# origin top-left (the same space Input.mouse_x/y report): filled rectangles,
# textured quads and text from a baked font atlas (tools/blender/font_build.py).
# Quads are batched into one buffer for the whole frame and uploaded ONCE at ov_end;
# a texture change only closes a draw range. (On Apple's GL every glBufferSubData
# flushes the context and waits for the GPU; uploading per texture change made a HUD
# with dozens of changes wait dozens of times a frame: 26 ms -> 40 ms, sampled.)
# ============================================================================
const OV_MAX_QUADS: int = 6000
const OV_FLOATS: int = 8 # x, y, u, v, r, g, b, a
var ov_prog: int = 0
var ov_mesh: Mesh = null
var ov_vbo: int = 0
var ov_buf: pointer = null
var ov_n: int = 0
var ov_tex: int = 0
var ov_mode: int = 2 # what the next quads read: 0 the image texture, 1 the font, 2 a flat colour
var ov_voff: int = 0 # float bits: 4 x ov_mode, added to v (overlay.frag)
var ov_white: int = 0
var ov_font: int = 0
var ov_font_adv: words = null # float bits, em units, one per glyph in the atlas
var ov_font_n: int = 0 # glyphs in the atlas
# Text is UTF-8, and the atlas says which code points it holds. font.json's "codes" lists
# them in atlas order; an older atlas without it is ASCII from "first" (32) on, which is
# what every font built before this was. ov_font_map[cp] is glyph + 1 for a code point
# under OV_MAP_N (0: not in the atlas); anything above is looked up in the sorted tail.
const OV_MAP_N: int = 8192
var ov_font_map: words = null
var ov_font_hi_cp: words = null # code points >= OV_MAP_N, ascending
var ov_font_hi_g: words = null
var ov_font_hi_n: int = 0
var ov_font_space: int = 0 # the glyph for ' ', and for what has none: '?'
var ov_font_qmark: int = 0
var ov_font_cols: int = 16
var ov_font_rows: int = 6
var ov_font_cell: int = 128 # px per cell in the atlas
var ov_font_em: int = 100 # px per em in the atlas
var ov_pad_x: int = 0 # float bits, em
var ov_base_y: int = 0
var ov_ready: bool = false
var ov_open: bool = false
var ov_dbg: bool = false
const OV_MAX_RANGES: int = 512
const OV_RANGE_W: int = 7 # texture, first quad, quad count, clip x, y, w, h (w = 0: none)
var ov_ranges: words = null
var ov_nr: int = 0
var ov_range_start: int = 0
var ov_clip_x: int = 0 # the current clip rectangle in screen pixels (top-left origin)
var ov_clip_y: int = 0
var ov_clip_w: int = 0
var ov_clip_h: int = 0
# The interface is sRGB. While the output is HDR10 the overlay draws with its HDR10 variant, which puts
# the interface at the picture's paper white; the SDR program there sent sRGB values as PQ, and the
# menu's yellow read as red on an HDR display.
var ov_prog_sdr: int = 0
var ov_prog_hdr: int = 0
# How bright the interface's white is while HDR10 is on, as a multiple of paper white. It is 1 for the
# interface; a calibration screen draws its test patches at a number of nits with ov_hdr_nits, which
# closes the batch so far - the multiple is one uniform per flush. On an SDR frame it does nothing.
var ov_hdr_scale: int = 0 # float bits; 0 until ov_begin sets 1
function ov_hdr_nits(nits: int) -> void {
var s = F_ONE
if nits != 0 { s = f_div(nits, r3d_hdr_paper_nits()) }
if s == ov_hdr_scale { return }
if ov_open { ov_flush() }
ov_hdr_scale = s
}
# back to the interface's own white
function ov_hdr_paper() -> void { ov_hdr_nits(0) }
function ov_pick_prog() -> void {
if gpu_hdr_active() {
if ov_prog_hdr == 0 {
r3d_program_log("overlay.vert", "overlay.frag", "#define HDR10\n")
ov_prog_hdr = gpu_program("#version 410 core\n#define HDR10\n" + r3d_shader_file("overlay.vert"), "#version 410 core\n#define HDR10\n" + r3d_shader_file("overlay.frag"), "overlay.vert", "overlay.frag", "#define HDR10\n")
}
if ov_prog_hdr != 0 { ov_prog = ov_prog_hdr; return }
}
ov_prog = ov_prog_sdr
}
function overlay_init(font_dir: string) -> bool {
r3d_program_log("overlay.vert", "overlay.frag", "")
ov_prog = gpu_program("#version 410 core\n" + r3d_shader_file("overlay.vert"), "#version 410 core\n" + r3d_shader_file("overlay.frag"), "overlay.vert", "overlay.frag", "")
if ov_prog == 0 { print("overlay: program failed"); return false }
ov_prog_sdr = ov_prog
ov_mesh = gpu_mesh_new()
ov_vbo = gpu_mesh_vertices(ov_mesh, null, gl_bytes_of(OV_MAX_QUADS * 6 * OV_FLOATS), GPU_DYNAMIC)
gpu_mesh_attr(ov_mesh, 0, 2, GPU_F32, OV_FLOATS * 4, 0, false)
gpu_mesh_attr(ov_mesh, 1, 2, GPU_F32, OV_FLOATS * 4, 8, false)
gpu_mesh_attr(ov_mesh, 2, 4, GPU_F32, OV_FLOATS * 4, 16, false)
gpu_mesh_done(ov_mesh)
ov_buf = gl_floats(OV_MAX_QUADS * 6 * OV_FLOATS)
ov_ranges = words(OV_MAX_RANGES * OV_RANGE_W)
ov_white = tex_solid(255, 255, 255, 255)
overlay_font(font_dir)
if ov_font == 0 { print("overlay: no font atlas, text disabled") }
ov_dbg = Os.has_env("R3D_FONTDBG")
ov_ready = true
return true
}
# Load (or swap to) the font atlas in `font_dir`: font.png and font.json. A game calls it
# again to change fonts at run time - a language whose script the default atlas does not
# carry brings its own. Returns false, and keeps the font it had, if there is none there.
function overlay_font(font_dir: string) -> bool {
let meta = Fs.read_text(font_dir + "/font.json")
if meta == null { return false }
let tex = tex_load_ex(font_dir + "/font.png", false, 0)
if tex == 0 { return false }
let j = Json.parse(meta)
ov_font_cols = jint(j, "cols", 16); ov_font_rows = jint(j, "rows", 6)
ov_font_cell = f_to_int(jnum(value_get(j, "cell"))); ov_font_em = f_to_int(jnum(value_get(j, "em")))
ov_pad_x = fl(0.14); ov_base_y = fl(0.30)
if value_has(j, "pad_x") != 0 { ov_pad_x = jnum(value_get(j, "pad_x")) }
if value_has(j, "base_y") != 0 { ov_base_y = jnum(value_get(j, "base_y")) }
let adv = value_get(j, "adv")
let n = value_count(adv)
let first = jint(j, "first", 32)
var codes: Val = null
if value_has(j, "codes") != 0 { codes = value_get(j, "codes") }
ov_font_n = n
ov_font_adv = words(n + 1)
for i in 0 .. n { ov_font_adv[i] = jnum(value_at(adv, i)) }
ov_font_map = words(OV_MAP_N)
for c in 0 .. OV_MAP_N { ov_font_map[c] = 0 }
ov_font_hi_cp = words(n + 1); ov_font_hi_g = words(n + 1); ov_font_hi_n = 0
for i in 0 .. n {
var cp = first + i
if codes != null and i < value_count(codes) { cp = f_to_int(jnum(value_at(codes, i))) }
if cp >= 0 and cp < OV_MAP_N { ov_font_map[cp] = i + 1 }
else if cp >= OV_MAP_N {
# kept ascending: the builder writes code points in order, so this is an append
ov_font_hi_cp[ov_font_hi_n] = cp; ov_font_hi_g[ov_font_hi_n] = i; ov_font_hi_n += 1
}
}
ov_font_space = 0
if ov_font_map[32] > 0 { ov_font_space = ov_font_map[32] - 1 }
ov_font_qmark = ov_font_space
if ov_font_map[63] > 0 { ov_font_qmark = ov_font_map[63] - 1 }
ov_font = tex
return true
}
# The code point starting at sp[i], with the bytes it took in ov_u8_len. A malformed or
# cut-off sequence is one byte of '?', so a string sliced mid-character still draws.
var ov_u8_len: int = 1
function ov_u8(sp: pointer, i: int, n: int) -> int {
let b0 = sp[i] & 255
ov_u8_len = 1
if b0 < 128 { return b0 }
if b0 >= 240 and b0 < 248 and i + 3 < n {
ov_u8_len = 4
return ((b0 & 7) << 18) | ((sp[i + 1] & 63) << 12) | ((sp[i + 2] & 63) << 6) | (sp[i + 3] & 63)
}
if b0 >= 224 and b0 < 240 and i + 2 < n {
ov_u8_len = 3
return ((b0 & 15) << 12) | ((sp[i + 1] & 63) << 6) | (sp[i + 2] & 63)
}
if b0 >= 192 and b0 < 224 and i + 1 < n {
ov_u8_len = 2
return ((b0 & 31) << 6) | (sp[i + 1] & 63)
}
return 63
}
# the atlas glyph for a code point: a control character is a space, a missing one is '?'
function ov_glyph(cp: int) -> int {
if cp < 32 { return ov_font_space }
if cp < OV_MAP_N {
let g = ov_font_map[cp]
if g > 0 { return g - 1 }
return ov_font_qmark
}
var lo = 0
var hi = ov_font_hi_n - 1
while lo <= hi {
let mid = (lo + hi) / 2
if ov_font_hi_cp[mid] == cp { return ov_font_hi_g[mid] }
if ov_font_hi_cp[mid] < cp { lo = mid + 1 } else { hi = mid - 1 }
}
return ov_font_qmark
}
# start drawing onto the screen: blending on, depth off
function ov_begin() -> void {
if not ov_ready { return }
gpu_fb_bind(gpu_screen_fb())
gpu_viewport(0, 0, gl_w, gl_h)
gpu_depth_test(false)
gpu_cull(false)
gpu_blend(true)
gpu_blend_func(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
ov_pick_prog()
gpu_use_program(ov_prog)
u_f2(gpu_uniform(ov_prog, "u_screen"), fi(gl_w), fi(gl_h))
ov_n = 0; ov_nr = 0; ov_range_start = 0
ov_tex = ov_white
ov_mode = 2; ov_voff = fi(8)
ov_clip_w = 0
ov_hdr_scale = F_ONE
ov_open = true
}
# everything drawn until ov_unclip stays inside this rectangle (a scrolling list)
function ov_clip(x: int, y: int, w: int, h: int) -> void {
ov_close_range()
ov_clip_x = x; ov_clip_y = y; ov_clip_w = w; ov_clip_h = h
}
function ov_unclip() -> void { ov_close_range(); ov_clip_w = 0 }
# close the current draw range (quads since its start, with the current texture)
function ov_close_range() -> void {
let n = ov_n - ov_range_start
if n <= 0 { return }
if ov_nr >= OV_MAX_RANGES { ov_flush(); return }
let o = ov_nr * OV_RANGE_W
ov_ranges[o] = ov_tex; ov_ranges[o + 1] = ov_range_start; ov_ranges[o + 2] = n
ov_ranges[o + 3] = ov_clip_x; ov_ranges[o + 4] = ov_clip_y; ov_ranges[o + 5] = ov_clip_w; ov_ranges[o + 6] = ov_clip_h
ov_nr += 1
ov_range_start = ov_n
}
# one upload of everything batched so far, then a draw per range
function ov_flush() -> void {
ov_close_range()
if ov_n == 0 { ov_nr = 0; ov_range_start = 0; return }
# The overlay draws onto the screen, whatever was bound since ov_begin: a render-scale change
# rebuilds the scene targets mid-frame and leaves framebuffer 0 bound, and on a headless run
# (where the screen is an offscreen framebuffer) every overlay draw after it was an invalid
# framebuffer operation. Saying the target at each flush is what a render pass says anyway.
gpu_fb_bind(gpu_screen_fb())
gpu_viewport(0, 0, gl_w, gl_h)
ov_pick_prog()
gpu_use_program(ov_prog)
gpu_mesh_bind(ov_mesh)
gpu_buffer_upload(ov_vbo, gl_bytes_of(ov_n * 6 * OV_FLOATS), ov_buf, GPU_STREAM)
var font = ov_font
if font == 0 { font = ov_white }
r3d_bind_2d(ov_prog, "u_font", 1, font)
var hs = ov_hdr_scale
if hs == 0 { hs = F_ONE }
u_f(gpu_uniform(ov_prog, "u_hdr_paper"), r3d_hdr_paper_nits())
u_f(gpu_uniform(ov_prog, "u_hdr_scale"), hs)
var last = -1
var clipped = false
for i in 0 .. ov_nr {
let o = i * OV_RANGE_W
let t = ov_ranges[o]
if t != last { r3d_bind_2d(ov_prog, "u_tex", 0, t); last = t }
if ov_ranges[o + 5] > 0 {
clipped = true
gpu_scissor(ov_ranges[o + 3], ov_ranges[o + 4], ov_ranges[o + 5], ov_ranges[o + 6])
} else if clipped { gpu_scissor_off(); clipped = false }
gpu_draw_range(ov_mesh, ov_ranges[o + 1] * 6, ov_ranges[o + 2] * 6)
}
if clipped { gpu_scissor_off() }
gpu_mesh_unbind()
ov_n = 0; ov_nr = 0; ov_range_start = 0
}
function ov_end() -> void {
if not ov_open { return }
ov_flush()
gpu_blend(false)
gpu_depth_test(true)
ov_open = false
}
# Text and flat panels never end a draw: the font atlas stays bound beside the image texture and each
# vertex says which it reads (ov_voff). Only a different image texture closes the range. A panel and
# its label used to alternate the white and font textures, and the HUD was 77-91 draws a frame.
function ov_use_tex(t: int) -> void {
if t != 0 and t == ov_font { ov_mode = 1; ov_voff = fi(4); return }
if t == ov_white { ov_mode = 2; ov_voff = fi(8); return }
ov_mode = 0; ov_voff = F_ZERO
if t != ov_tex { ov_close_range(); ov_tex = t }
}
# one vertex into the batch
function ov_vert(k: int, x: int, y: int, u: int, v: int, r: int, g: int, b: int, a: int) -> void {
let o = k * OV_FLOATS
gl_put_bits(ov_buf, o, x); gl_put_bits(ov_buf, o + 1, y)
gl_put_bits(ov_buf, o + 2, u); gl_put_bits(ov_buf, o + 3, f_add(v, ov_voff))
gl_put_bits(ov_buf, o + 4, r); gl_put_bits(ov_buf, o + 5, g); gl_put_bits(ov_buf, o + 6, b); gl_put_bits(ov_buf, o + 7, a)
}
# a textured quad, float-bit pixel corners and uvs
function ov_quad(x0: int, y0: int, x1: int, y1: int, u0: int, v0: int, u1: int, v1: int, r: int, g: int, b: int, a: int) -> void {
if ov_n >= OV_MAX_QUADS { ov_flush() }
let k = ov_n * 6
ov_vert(k, x0, y0, u0, v0, r, g, b, a)
ov_vert(k + 1, x1, y0, u1, v0, r, g, b, a)
ov_vert(k + 2, x1, y1, u1, v1, r, g, b, a)
ov_vert(k + 3, x0, y0, u0, v0, r, g, b, a)
ov_vert(k + 4, x1, y1, u1, v1, r, g, b, a)
ov_vert(k + 5, x0, y1, u0, v1, r, g, b, a)
ov_n += 1
}
# a filled rectangle at integer pixels; colour as float bits 0..1
function ov_rect(x: int, y: int, w: int, h: int, r: int, g: int, b: int, a: int) -> void {
ov_use_tex(ov_white)
ov_quad(fi(x), fi(y), fi(x + w), fi(y + h), F_ZERO, F_ZERO, F_ONE, F_ONE, r, g, b, a)
}
function ov_frame(x: int, y: int, w: int, h: int, t: int, r: int, g: int, b: int, a: int) -> void {
ov_rect(x, y, w, t, r, g, b, a)
ov_rect(x, y + h - t, w, t, r, g, b, a)
ov_rect(x, y, t, h, r, g, b, a)
ov_rect(x + w - t, y, t, h, r, g, b, a)
}
# a whole texture at integer pixels
function ov_image(tex: int, x: int, y: int, w: int, h: int, a: int) -> void {
ov_use_tex(tex)
ov_quad(fi(x), fi(y), fi(x + w), fi(y + h), F_ZERO, F_ZERO, F_ONE, F_ONE, F_ONE, F_ONE, F_ONE, a)
}
# the width in pixels of `s` at `size` pixels per em
function ov_text_w(size: int, s: string) -> int {
if ov_font_adv == null { return 0 }
var w = F_ZERO
let sp: pointer = s # UTF-8 bytes, not one-character strings
let n = len(sp)
var i = 0
while i < n {
let g = ov_glyph(ov_u8(sp, i, n))
i += ov_u8_len
w = f_add(w, f_mul(ov_font_adv[g], fi(size)))
}
return f_to_int(w)
}
# text with its top-left at (x, y); returns the pen x after it
function ov_text(x: int, y: int, size: int, s: string, r: int, g: int, b: int, a: int) -> int {
if ov_font == 0 { return x }
ov_use_tex(ov_font)
let k = fr(size, ov_font_em) # atlas px -> screen px
let cell = f_mul(fi(ov_font_cell), k)
var pen = fi(x)
let base = f_add(fi(y), f_mul(fi(size), fl(0.80)))
let px = f_mul(f_mul(ov_pad_x, fi(ov_font_em)), k)
let py = f_mul(f_mul(ov_base_y, fi(ov_font_em)), k)
let sp: pointer = s
let n = len(sp)
var i = 0
while i < n {
let c = ov_glyph(ov_u8(sp, i, n))
i += ov_u8_len
if c != ov_font_space {
let cx = c - (c / ov_font_cols) * ov_font_cols
let cy = c / ov_font_cols
let u0 = fr(cx, ov_font_cols); let u1 = fr(cx + 1, ov_font_cols)
let v0 = fr(cy, ov_font_rows); let v1 = fr(cy + 1, ov_font_rows)
let x0 = f_sub(pen, px); let y1 = f_add(base, py)
ov_quad(x0, f_sub(y1, cell), f_add(x0, cell), y1, u0, v0, u1, v1, r, g, b, a)
}
pen = f_add(pen, f_mul(ov_font_adv[c], fi(size)))
}
return f_to_int(pen)
}
# text with a soft dark shadow under it (HUD over a bright meadow)
function ov_text_sh(x: int, y: int, size: int, s: string, r: int, g: int, b: int, a: int) -> int {
let d = size / 18 + 1
ov_text(x + d, y + d, size, s, F_ZERO, F_ZERO, F_ZERO, f_mul(a, fl(0.7)))
return ov_text(x, y, size, s, r, g, b, a)
}
function ov_text_center(cx: int, y: int, size: int, s: string, r: int, g: int, b: int, a: int) -> void {
ov_text(cx - ov_text_w(size, s) / 2, y, size, s, r, g, b, a)
}
# text wrapped at `maxw` pixels on spaces; returns the y after the last line
function ov_text_wrap(x: int, y: int, size: int, maxw: int, s: string, r: int, g: int, b: int, a: int) -> int {
let sp: pointer = s
let n = len(sp)
var first = 0
var ly = y
while first < n {
var last_space = -1
var i = first
var stop = n
while i < n {
if sp[i] == 10 { stop = i; break }
if sp[i] == 32 { last_space = i }
let piece: string = s[first .. i + 1]
if ov_text_w(size, piece) > maxw and last_space > first { stop = last_space; break }
i += 1
}
let line: string = s[first .. stop]
ov_text(x, ly, size, line, r, g, b, a)
ly += size * 13 / 10
first = stop
while first < n and (sp[first] == 32 or sp[first] == 10) { first += 1 }
}
return ly
}
# ---- more shapes for a game's interface -------------------------------------------------
# a sub-rectangle of a texture (uv corners as float bits) tinted, at integer pixels
function ov_sub(tex: int, x: int, y: int, w: int, h: int, u0: int, v0: int, u1: int, v1: int, r: int, g: int, b: int, a: int) -> void {
ov_use_tex(tex)
ov_quad(fi(x), fi(y), fi(x + w), fi(y + h), u0, v0, u1, v1, r, g, b, a)
}
# a whole texture stretched by nine slices: corners `src` texels wide in a `tw` px square
# texture, drawn `dst` pixels wide, so rounded corners keep their shape at any size
function ov_nine(tex: int, tw: int, src: int, x: int, y: int, w: int, h: int, dst: int, r: int, g: int, b: int, a: int) -> void {
ov_use_tex(tex)
let s = fr(src, tw)
let xs = words(4); let ys = words(4); let us = words(4); let vs = words(4)
xs[0] = fi(x); xs[1] = fi(x + dst); xs[2] = fi(x + w - dst); xs[3] = fi(x + w)
ys[0] = fi(y); ys[1] = fi(y + dst); ys[2] = fi(y + h - dst); ys[3] = fi(y + h)
us[0] = F_ZERO; us[1] = s; us[2] = f_sub(F_ONE, s); us[3] = F_ONE
vs[0] = F_ZERO; vs[1] = s; vs[2] = f_sub(F_ONE, s); vs[3] = F_ONE
for j in 0 .. 3 {
for i in 0 .. 3 { ov_quad(xs[i], ys[j], xs[i + 1], ys[j + 1], us[i], vs[j], us[i + 1], vs[j + 1], r, g, b, a) }
}
free(xs); free(ys); free(us); free(vs)
}
# an arbitrary quad (float-bit pixel corners, clockwise from top-left) of a texture
function ov_quad4(x0: int, y0: int, x1: int, y1: int, x2: int, y2: int, x3: int, y3: int, u0: int, v0: int, u1: int, v1: int, r: int, g: int, b: int, a: int) -> void {
if ov_n >= OV_MAX_QUADS { ov_flush() }
let k = ov_n * 6
ov_vert(k, x0, y0, u0, v0, r, g, b, a)
ov_vert(k + 1, x1, y1, u1, v0, r, g, b, a)
ov_vert(k + 2, x2, y2, u1, v1, r, g, b, a)
ov_vert(k + 3, x0, y0, u0, v0, r, g, b, a)
ov_vert(k + 4, x2, y2, u1, v1, r, g, b, a)
ov_vert(k + 5, x3, y3, u0, v1, r, g, b, a)
ov_n += 1
}
# a line of thickness `t` pixels between two points (float-bit pixels)
function ov_line(x0: int, y0: int, x1: int, y1: int, t: int, r: int, g: int, b: int, a: int) -> void {
ov_use_tex(ov_white)
let dx = f_sub(x1, x0); let dy = f_sub(y1, y0)
let l = f_max(f_sqrt(f_add(f_mul(dx, dx), f_mul(dy, dy))), fl(0.001))
let nx = f_mul(f_div(f_neg(dy), l), f_mul(t, F_HALF)); let ny = f_mul(f_div(dx, l), f_mul(t, F_HALF))
ov_quad4(f_add(x0, nx), f_add(y0, ny), f_add(x1, nx), f_add(y1, ny), f_sub(x1, nx), f_sub(y1, ny), f_sub(x0, nx), f_sub(y0, ny), F_ZERO, F_ZERO, F_ONE, F_ONE, r, g, b, a)
}
# a sub-rectangle of a texture rotated by `ang` radians about its centre (cx, cy), `w` x `h` pixels
function ov_sub_rot(tex: int, cx: int, cy: int, w: int, h: int, ang: int, u0: int, v0: int, u1: int, v1: int, r: int, g: int, b: int, a: int) -> void {
ov_use_tex(tex)
let c = f_cos(ang); let s = f_sin(ang)
let hw = f_mul(fi(w), F_HALF); let hh = f_mul(fi(h), F_HALF)
let fx = fi(cx); let fy = fi(cy)
# corners: (-hw,-hh) (hw,-hh) (hw,hh) (-hw,hh) rotated
let x0 = f_add(fx, f_sub(f_mul(f_neg(hw), c), f_mul(f_neg(hh), s))); let y0 = f_add(fy, f_add(f_mul(f_neg(hw), s), f_mul(f_neg(hh), c)))
let x1 = f_add(fx, f_sub(f_mul(hw, c), f_mul(f_neg(hh), s))); let y1 = f_add(fy, f_add(f_mul(hw, s), f_mul(f_neg(hh), c)))
let x2 = f_add(fx, f_sub(f_mul(hw, c), f_mul(hh, s))); let y2 = f_add(fy, f_add(f_mul(hw, s), f_mul(hh, c)))
let x3 = f_add(fx, f_sub(f_mul(f_neg(hw), c), f_mul(hh, s))); let y3 = f_add(fy, f_add(f_mul(f_neg(hw), s), f_mul(hh, c)))
ov_quad4(x0, y0, x1, y1, x2, y2, x3, y3, u0, v0, u1, v1, r, g, b, a)
}
# a filled circle approximated by `n` wedges (float-bit centre and radius)
function ov_disc(cx: int, cy: int, rad: int, n: int, r: int, g: int, b: int, a: int) -> void {
ov_use_tex(ov_white)
let step = f_div(f_mul(F_TWO, F_PI), fi(n))
for i in 0 .. n {
let a0 = f_mul(fi(i), step); let a1 = f_add(a0, step)
let ax = f_add(cx, f_mul(f_cos(a0), rad)); let ay = f_add(cy, f_mul(f_sin(a0), rad))
let bx = f_add(cx, f_mul(f_cos(a1), rad)); let by = f_add(cy, f_mul(f_sin(a1), rad))
ov_quad4(cx, cy, ax, ay, bx, by, cx, cy, F_ZERO, F_ZERO, F_ONE, F_ONE, r, g, b, a)
}
}
# a ring: `n` segments of thickness `t`, from angle a0 for `span` radians (float bits)
function ov_arc(cx: int, cy: int, rad: int, t: int, a0: int, span: int, n: int, r: int, g: int, b: int, a: int) -> void {
let step = f_div(span, fi(n))
for i in 0 .. n {
let b0 = f_add(a0, f_mul(fi(i), step)); let b1 = f_add(b0, step)
ov_line(f_add(cx, f_mul(f_cos(b0), rad)), f_add(cy, f_mul(f_sin(b0), rad)), f_add(cx, f_mul(f_cos(b1), rad)), f_add(cy, f_mul(f_sin(b1), rad)), t, r, g, b, a)
}
}