feat(lang): strict numbers in float files; render3d on float

A numbers float file adapts decimal literals to a fixed operand or slot, and refuses to
promote a computed int to a float implicitly: there it is almost always float bits. Explicit
float(x) is always allowed.

render3d's numbers are float, converted by tools/migrate/floatbits.py - a whole-program
inference of which ints carried IEEE bits (union-find over flows, calls, returns, buffers,
nested buffers and lexical scopes) and a rewriter to operators, Math.* and float literals,
with float_bits / float_from_bits left only where bits really cross (runtime scratch
buffers, mixed buffers). Seed regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-23 17:13:25 +03:00
parent 3ac0d8d5cb
commit cc89fc37a4
35 changed files with 57282 additions and 54382 deletions

View file

@ -18,10 +18,10 @@ 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_voff: float = 0.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_adv: floats = 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
@ -38,8 +38,8 @@ 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_pad_x: float = 0.0 # float bits, em
var ov_base_y: float = 0.0
var ov_ready: bool = false
var ov_open: bool = false
var ov_dbg: bool = false
@ -61,16 +61,16 @@ 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()) }
var ov_hdr_scale: float = 0.0 # float bits; 0 until ov_begin sets 1
function ov_hdr_nits(nits: float) -> void {
var s = 1.0
if nits != 0.0 { s = 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_hdr_paper() -> void { ov_hdr_nits(0.0) }
function ov_pick_prog() -> void {
if gpu_hdr_active() {
if ov_prog_hdr == 0 {
@ -113,8 +113,8 @@ function overlay_font(font_dir: string) -> bool {
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)
ov_font_cell = int(jnum(value_get(j, "cell"))); ov_font_em = int(jnum(value_get(j, "em")))
ov_pad_x = 0.14; ov_base_y = 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")
@ -123,14 +123,14 @@ function overlay_font(font_dir: string) -> bool {
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)
ov_font_adv = floats(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 codes != null and i < value_count(codes) { cp = 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
@ -195,12 +195,12 @@ function ov_begin() -> void {
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))
u_f2(gpu_uniform(ov_prog, "u_screen"), float(gl_w), float(gl_h))
ov_n = 0; ov_nr = 0; ov_range_start = 0
ov_tex = ov_white
ov_mode = 2; ov_voff = fi(8)
ov_mode = 2; ov_voff = 8.0
ov_clip_w = 0
ov_hdr_scale = F_ONE
ov_hdr_scale = 1.0
ov_open = true
}
# everything drawn until ov_unclip stays inside this rectangle (a scrolling list)
@ -238,7 +238,7 @@ function ov_flush() -> void {
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 }
if hs == 0.0 { hs = 1.0 }
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
@ -268,21 +268,21 @@ function ov_end() -> void {
# 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 != 0 and t == ov_font { ov_mode = 1; ov_voff = 4.0; return }
if t == ov_white { ov_mode = 2; ov_voff = 8.0; return }
ov_mode = 0; ov_voff = 0.0
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 {
function ov_vert(k: int, x: float, y: float, u: float, v: float, r: float, g: float, b: float, a: float) -> 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)
gl_put_bits(ov_buf, o, float_bits(x)); gl_put_bits(ov_buf, o + 1, float_bits(y))
gl_put_bits(ov_buf, o + 2, float_bits(u)); gl_put_bits(ov_buf, o + 3, float_bits(v + ov_voff))
gl_put_bits(ov_buf, o + 4, float_bits(r)); gl_put_bits(ov_buf, o + 5, float_bits(g)); gl_put_bits(ov_buf, o + 6, float_bits(b)); gl_put_bits(ov_buf, o + 7, float_bits(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 {
function ov_quad(x0: float, y0: float, x1: float, y1: float, u0: float, v0: float, u1: float, v1: float, r: float, g: float, b: float, a: float) -> 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)
@ -295,46 +295,46 @@ function ov_quad(x0: int, y0: int, x1: int, y1: int, u0: int, v0: int, u1: int,
}
# 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 {
function ov_rect(x: int, y: int, w: int, h: int, r: float, g: float, b: float, a: float) -> 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)
ov_quad(float(x), float(y), float(x + w), float(y + h), 0.0, 0.0, 1.0, 1.0, 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 {
function ov_frame(x: int, y: int, w: int, h: int, t: int, r: float, g: float, b: float, a: float) -> 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 {
function ov_image(tex: int, x: int, y: int, w: int, h: int, a: float) -> 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)
ov_quad(float(x), float(y), float(x + w), float(y + h), 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 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
var w = 0.0
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)))
w = w + ov_font_adv[g] * float(size)
}
return f_to_int(w)
return 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 {
function ov_text(x: int, y: int, size: int, s: string, r: float, g: float, b: float, a: float) -> 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 k = float(size) / float(ov_font_em) # atlas px -> screen px
let cell = float(ov_font_cell) * k
var pen = float(x)
let base = float(y) + float(size) * 0.80
let px = ov_pad_x * float(ov_font_em) * k
let py = ov_base_y * float(ov_font_em) * k
let sp: pointer = s
let n = len(sp)
var i = 0
@ -344,27 +344,27 @@ function ov_text(x: int, y: int, size: int, s: string, r: int, g: int, b: int, a
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)
let u0 = float(cx) / float(ov_font_cols); let u1 = float(cx + 1) / float(ov_font_cols)
let v0 = float(cy) / float(ov_font_rows); let v1 = float(cy + 1) / float(ov_font_rows)
let x0 = pen - px; let y1 = base + py
ov_quad(x0, y1 - cell, x0 + cell, y1, u0, v0, u1, v1, r, g, b, a)
}
pen = f_add(pen, f_mul(ov_font_adv[c], fi(size)))
pen = pen + ov_font_adv[c] * float(size)
}
return f_to_int(pen)
return 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 {
function ov_text_sh(x: int, y: int, size: int, s: string, r: float, g: float, b: float, a: float) -> 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)))
ov_text(x + d, y + d, size, s, 0.0, 0.0, 0.0, a * 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 {
function ov_text_center(cx: int, y: int, size: int, s: string, r: float, g: float, b: float, a: float) -> 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 {
function ov_text_wrap(x: int, y: int, size: int, maxw: int, s: string, r: float, g: float, b: float, a: float) -> int {
let sp: pointer = s
let n = len(sp)
var first = 0
@ -391,27 +391,27 @@ function ov_text_wrap(x: int, y: int, size: int, maxw: int, s: string, r: int, g
# ---- 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 {
function ov_sub(tex: int, x: int, y: int, w: int, h: int, u0: float, v0: float, u1: float, v1: float, r: float, g: float, b: float, a: float) -> 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)
ov_quad(float(x), float(y), float(x + w), float(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 {
function ov_nine(tex: int, tw: int, src: int, x: int, y: int, w: int, h: int, dst: int, r: float, g: float, b: float, a: float) -> 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
let s = float(src) / float(tw)
let xs = floats(4); let ys = floats(4); let us = floats(4); let vs = floats(4)
xs[0] = float(x); xs[1] = float(x + dst); xs[2] = float(x + w - dst); xs[3] = float(x + w)
ys[0] = float(y); ys[1] = float(y + dst); ys[2] = float(y + h - dst); ys[3] = float(y + h)
us[0] = 0.0; us[1] = s; us[2] = 1.0 - s; us[3] = 1.0
vs[0] = 0.0; vs[1] = s; vs[2] = 1.0 - s; vs[3] = 1.0
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 {
function ov_quad4(x0: float, y0: float, x1: float, y1: float, x2: float, y2: float, x3: float, y3: float, u0: float, v0: float, u1: float, v1: float, r: float, g: float, b: float, a: float) -> 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)
@ -423,42 +423,42 @@ function ov_quad4(x0: int, y0: int, x1: int, y1: int, x2: int, y2: int, x3: int,
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 {
function ov_line(x0: float, y0: float, x1: float, y1: float, t: float, r: float, g: float, b: float, a: float) -> 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)
let dx = x1 - x0; let dy = y1 - y0
let l = Math.max(Math.sqrt(dx * dx + dy * dy), 0.001)
let nx = -dy / l * (t * 0.5); let ny = dx / l * (t * 0.5)
ov_quad4(x0 + nx, y0 + ny, x1 + nx, y1 + ny, x1 - nx, y1 - ny, x0 - nx, y0 - ny, 0.0, 0.0, 1.0, 1.0, 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 {
function ov_sub_rot(tex: int, cx: int, cy: int, w: int, h: int, ang: float, u0: float, v0: float, u1: float, v1: float, r: float, g: float, b: float, a: float) -> 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)
let c = Math.cos(ang); let s = Math.sin(ang)
let hw = float(w) * 0.5; let hh = float(h) * 0.5
let fx = float(cx); let fy = float(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)))
let x0 = fx + (-hw * c - -hh * s); let y0 = fy + (-hw * s + -hh * c)
let x1 = fx + (hw * c - -hh * s); let y1 = fy + (hw * s + -hh * c)
let x2 = fx + (hw * c - hh * s); let y2 = fy + (hw * s + hh * c)
let x3 = fx + (-hw * c - hh * s); let y3 = fy + (-hw * s + 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 {
function ov_disc(cx: float, cy: float, rad: float, n: int, r: float, g: float, b: float, a: float) -> void {
ov_use_tex(ov_white)
let step = f_div(f_mul(F_TWO, F_PI), fi(n))
let step = 2.0 * PI / float(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)
let a0 = float(i) * step; let a1 = a0 + step
let ax = cx + Math.cos(a0) * rad; let ay = cy + Math.sin(a0) * rad
let bx = cx + Math.cos(a1) * rad; let by = cy + Math.sin(a1) * rad
ov_quad4(cx, cy, ax, ay, bx, by, cx, cy, 0.0, 0.0, 1.0, 1.0, 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))
function ov_arc(cx: float, cy: float, rad: float, t: float, a0: float, span: float, n: int, r: float, g: float, b: float, a: float) -> void {
let step = span / float(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)
let b0 = a0 + float(i) * step; let b1 = b0 + step
ov_line(cx + Math.cos(b0) * rad, cy + Math.sin(b0) * rad, cx + Math.cos(b1) * rad, cy + Math.sin(b1) * rad, t, r, g, b, a)
}
}