Merge commit '3f27235' into lang/foundations

This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 19:22:23 +03:00
commit e53a9fc865
15 changed files with 427 additions and 107 deletions

View file

@ -61,6 +61,8 @@ showing its own models - adds them from its own module with `def LabScenes <name
| `lab_settle(frames)`, `lab_font(dir)`, `lab_title(s)`, `lab_note(s)` | how long a shot settles; the overlay font for captions (none by default); the window's title; a line under the caption |
| `lab_scene()`, `lab_frame()`, `lab_time()` | the scene, and the lab's clock |
| `lab_ppm_to_png(ppm, png)` | a frame as a PNG (stored, not compressed) |
| `lab_png_write(path, w, h, channels, px)`, `lab_png_write_from(..., px, at)` | 8-bit pixels (1 grey, 2 grey + alpha, 3 RGB, 4 RGBA) as a PNG, the same way; `png_write.ludic` needs nothing else of the lab, so a program with a lab of its own imports it alone (`import "ludic.lab/png_write.ludic"`, state `LabPngState`) |
| `lab_px_r32f_norm`, `lab_px_rg16f`, `lab_px_rgba16f_hdr` (`png_convert.ludic`) | previews of float textures as 8-bit pixels: R32F min..max to black..white; RG16F as red and green x255, blue 0; HDR RGBA16F as x/(1+x) then sRGB, alpha dropped |
## The plate's files

View file

@ -7,6 +7,8 @@ import "state.ludic"
import "scenes.ludic"
import "plate.ludic"
import "shots.ludic"
import "png_write.ludic"
import "png_convert.ludic"
import "png.ludic"
import "boot.ludic"
import "loop.ludic"

View file

@ -1,39 +1,6 @@
# png.ludic - the renderer writes a frame as a PPM; the lab keeps it as a PNG, stored rather than
# compressed (a PNG anything opens, with no zlib here): the shot is the PPM's pixels, filter 0
# on every row, in deflate's stored blocks, with its CRCs and its Adler-32.
# compressed (png_write.ludic: a PNG anything opens, with no zlib here).
# the CRC-32 table, made once with the state
function lp_crc_table() -> []long {
let t = new []long
let poly: long = 3988292384 # 0xEDB88320: eight hex digits would be a 32-bit pattern
for n in 0 .. 256 {
var c = long(n)
for k in 0 .. 8 {
if (c & long(1)) != long(0) { c = (c >> long(1)) ^ poly } else { c = c >> long(1) }
}
push(t, c)
}
return t
}
# CRC-32 of out[from .. from + n)
function lp_crc(lab_st: mut LabState, out: []byte, from: int, n: int) -> long {
let mask: long = 4294967295
var c = mask
for i in from .. from + n {
let k = int((c ^ long(out[i])) & long(255))
c = lab_st.lp_table[k] ^ (c >> long(8))
}
return c ^ mask
}
function lp_put32(out: []byte, at: int, v: long) -> void {
out[at] = int((v >> long(24)) & long(255))
out[at + 1] = int((v >> long(16)) & long(255))
out[at + 2] = int((v >> long(8)) & long(255))
out[at + 3] = int(v & long(255))
}
function lp_tag(out: []byte, at: int, tag: string) -> void {
for i in 0 .. 4 { out[at + i] = tag[i] }
}
# a PPM's header: "P6", width, height, maxval, each after white space; returns the pixel offset
function lp_ppm_header(lab_st: mut LabState, d: []byte) -> int {
var i = 2
@ -60,68 +27,10 @@ export function lab_ppm_to_png(lab_st: mut LabState, ppm: string, png: string) -
let px = lp_ppm_header(lab_st, d)
let w = lab_st.lp_w
let h = lab_st.lp_h
let row = w * 3 + 1
let raw = row * h
let blocks = (raw + 65534) / 65535
let idat = 2 + blocks * 5 + raw + 4
let total = 8 + 25 + 12 + idat + 12
let out = buffer(total)
let sig = [137, 80, 78, 71, 13, 10, 26, 10]
for i in 0 .. 8 { out[i] = sig[i] }
var at = 8
lp_put32(out, at, long(13))
lp_tag(out, at + 4, "IHDR")
lp_put32(out, at + 8, long(w))
lp_put32(out, at + 12, long(h))
out[at + 16] = 8
out[at + 17] = 2
out[at + 18] = 0
out[at + 19] = 0
out[at + 20] = 0
lp_put32(out, at + 21, lp_crc(lab_st, out, at + 4, 17))
at += 25
lp_put32(out, at, long(idat))
lp_tag(out, at + 4, "IDAT")
var o = at + 8
out[o] = 120
out[o + 1] = 1
o += 2
var a = 1
var b = 0
var left = raw
var k = 0 # the byte of the raw stream being written
while left > 0 {
let n = Math.min(left, 65535)
left -= n
var fin = 0
if left == 0 { fin = 1 }
out[o] = fin
out[o + 1] = n & 255
out[o + 2] = (n >> 8) & 255
out[o + 3] = (~n) & 255
out[o + 4] = ((~n) >> 8) & 255
o += 5
for j in 0 .. n {
let y = k / row
let x = k % row
var v = 0
if x > 0 { v = d[px + y * w * 3 + x - 1] }
out[o] = v
a = (a + v) % 65521
b = (b + a) % 65521
o += 1
k += 1
}
}
lp_put32(out, o, long(b) * long(65536) + long(a))
o += 4
lp_put32(out, o, lp_crc(lab_st, out, at + 4, o - at - 4))
o += 4
lp_put32(out, o, long(0))
lp_tag(out, o + 4, "IEND")
lp_put32(out, o + 8, lp_crc(lab_st, out, o + 4, 4))
o += 12
if not Fs.write_bytes(png, out, o) { return false }
if px + w * h * 3 > len(d) { return false }
let out = buffer(lp_png_size(w, h, 3))
let n = lp_encode(lab_st.lp_table, w, h, 3, d, px, out)
if not Fs.write_bytes(png, out, n) { return false }
Fs.remove(ppm)
return true
}

View file

@ -0,0 +1,71 @@
# png_convert.ludic - previews of textures that are not 8-bit, as 8-bit pixels for lab_png_write. Each says
# its mapping, because a preview is only honest when you know what a grey level means. Little-endian
# samples as a GPU stores them; nothing here is ever read back into a texture.
# the 32-bit float at src[at]
export function lab_f32_at(src: []byte, at: int) -> float {
let bits = (src[at] & 255) | ((src[at + 1] & 255) << 8) | ((src[at + 2] & 255) << 16) | ((src[at + 3] & 255) << 24)
return float_from_bits(bits)
}
# the 16-bit half float at src[at]; infinities and NaNs read as the largest half (65504)
export function lab_f16_at(src: []byte, at: int) -> float {
let h = (src[at] & 255) | ((src[at + 1] & 255) << 8)
let e = (h >> 10) & 31
let m = float(h & 1023)
var v = 65504.0
if e == 0 { v = m * Math.pow(2.0, -24.0) }
if e > 0 and e < 31 { v = (1.0 + m / 1024.0) * Math.pow(2.0, float(e - 15)) }
if (h & 32768) != 0 { return -v }
return v
}
function lp_byte(x: float) -> int { return int(Math.clamp(x * 255.0 + 0.5, 0.0, 255.0)) }
# n R32F samples at src[at] as grey: the smallest value black, the largest white, linear between
# (a flat field is black). The range is the image's own, so two previews are not on one scale
@alloc_ok("a bake's preview, once")
export function lab_px_r32f_norm(src: []byte, at: int, n: int) -> []byte {
var lo = lab_f32_at(src, at)
var hi = lo
for i in 0 .. n {
let v = lab_f32_at(src, at + i * 4)
lo = Math.min(lo, v)
hi = Math.max(hi, v)
}
let out = buffer(n)
var span = hi - lo
if span <= 0.0 { span = 1.0 }
for i in 0 .. n { out[i] = lp_byte((lab_f32_at(src, at + i * 4) - lo) / span) }
return out
}
# n RG16F samples at src[at] as RGB: red and green each x255 clamped to 0..1, blue 0 (a table of two
# numbers in 0..1, such as the BRDF lookup or a distance and a mask)
@alloc_ok("a bake's preview, once")
export function lab_px_rg16f(src: []byte, at: int, n: int) -> []byte {
let out = buffer(n * 3)
for i in 0 .. n {
out[i * 3] = lp_byte(lab_f16_at(src, at + i * 4))
out[i * 3 + 1] = lp_byte(lab_f16_at(src, at + i * 4 + 2))
out[i * 3 + 2] = 0
}
return out
}
# sRGB's encoding of a linear value in 0..1
function lp_srgb(x: float) -> float {
if x <= 0.0031308 { return x * 12.92 }
return 1.055 * Math.pow(x, 1.0 / 2.4) - 0.055
}
# n RGBA16F samples at src[at] (linear HDR light) as RGB: each channel x/(1+x) (Reinhard: 1.0 lands at
# half and nothing clips), then sRGB-encoded; alpha dropped. No exposure is applied
@alloc_ok("a bake's preview, once")
export function lab_px_rgba16f_hdr(src: []byte, at: int, n: int) -> []byte {
let out = buffer(n * 3)
for i in 0 .. n {
for c in 0 .. 3 {
let x = Math.max(lab_f16_at(src, at + i * 8 + c * 2), 0.0)
out[i * 3 + c] = lp_byte(lp_srgb(x / (1.0 + x)))
}
}
return out
}

View file

@ -0,0 +1,133 @@
# png_write.ludic - raw 8-bit pixels (1 to 4 channels) as a PNG anything opens: filter 0 on every row,
# deflate's stored blocks (no zlib here), the CRCs and the Adler-32. Needs nothing else of the lab, so a
# program with a lab of its own imports this file alone: import "ludic.lab/png_write.ludic"
export state LabPngState {
table: []long = lp_crc_table()
}
# the CRC-32 table, made once with a state
function lp_crc_table() -> []long {
let t = new []long
let poly: long = 3988292384 # 0xEDB88320: eight hex digits would be a 32-bit pattern
for n in 0 .. 256 {
var c = long(n)
for k in 0 .. 8 {
if (c & long(1)) != long(0) { c = (c >> long(1)) ^ poly } else { c = c >> long(1) }
}
push(t, c)
}
return t
}
# CRC-32 of out[from .. from + n)
function lp_crc_of(table: []long, out: []byte, from: int, n: int) -> long {
let mask: long = 4294967295
var c = mask
for i in from .. from + n {
let k = int((c ^ long(out[i])) & long(255))
c = table[k] ^ (c >> long(8))
}
return c ^ mask
}
function lp_put32(out: []byte, at: int, v: long) -> void {
out[at] = int((v >> long(24)) & long(255))
out[at + 1] = int((v >> long(16)) & long(255))
out[at + 2] = int((v >> long(8)) & long(255))
out[at + 3] = int(v & long(255))
}
function lp_tag(out: []byte, at: int, tag: string) -> void {
for i in 0 .. 4 { out[at + i] = tag[i] }
}
# PNG's colour type for 1..4 channels: grey, grey + alpha, RGB, RGBA
function lp_colour_type(channels: int) -> int {
if channels == 1 { return 0 }
if channels == 2 { return 4 }
if channels == 3 { return 2 }
return 6
}
# the header, then the pixels, then the end, into one buffer; the length written, or -1
@alloc_ok("a bake's preview or a lab shot: one image written, once")
function lp_encode(table: []long, w: int, h: int, channels: int, px: []byte, at: int, out: []byte) -> int {
let sig = [137, 80, 78, 71, 13, 10, 26, 10]
for i in 0 .. 8 { out[i] = sig[i] }
lp_put32(out, 8, long(13))
lp_tag(out, 12, "IHDR")
lp_put32(out, 16, long(w))
lp_put32(out, 20, long(h))
out[24] = 8
out[25] = lp_colour_type(channels)
out[26] = 0
out[27] = 0
out[28] = 0
lp_put32(out, 29, lp_crc_of(table, out, 12, 17))
let o = lp_idat(table, w, h, channels, px, at, out, 33)
lp_put32(out, o, long(0))
lp_tag(out, o + 4, "IEND")
lp_put32(out, o + 8, lp_crc_of(table, out, o + 4, 4))
return o + 12
}
# the IDAT chunk at `at0`: every row behind a filter byte of 0, in stored blocks; where it ends
function lp_idat(table: []long, w: int, h: int, channels: int, px: []byte, at: int, out: []byte, at0: int) -> int {
let stride = w * channels
let row = stride + 1
let raw = row * h
let blocks = (raw + 65534) / 65535
lp_put32(out, at0, long(2 + blocks * 5 + raw + 4))
lp_tag(out, at0 + 4, "IDAT")
var o = at0 + 8
out[o] = 120
out[o + 1] = 1
o += 2
var a = 1
var b = 0
var left = raw
var k = 0 # the byte of the raw stream being written
while left > 0 {
let n = Math.min(left, 65535)
left -= n
var fin = 0
if left == 0 { fin = 1 }
out[o] = fin
out[o + 1] = n & 255
out[o + 2] = (n >> 8) & 255
out[o + 3] = (~n) & 255
out[o + 4] = ((~n) >> 8) & 255
o += 5
for j in 0 .. n {
let x = k % row
var v = 0
if x > 0 { v = px[at + (k / row) * stride + x - 1] & 255 }
out[o] = v
a = (a + v) % 65521
b = (b + a) % 65521
o += 1
k += 1
}
}
lp_put32(out, o, long(b) * long(65536) + long(a))
o += 4
lp_put32(out, o, lp_crc_of(table, out, at0 + 4, o - at0 - 4))
return o + 4
}
# the bytes a PNG of this size takes, stored
function lp_png_size(w: int, h: int, channels: int) -> int {
let raw = (w * channels + 1) * h
return 8 + 25 + 12 + 2 + ((raw + 65534) / 65535) * 5 + raw + 4 + 12
}
# w x h pixels of `channels` 8-bit samples each (1 grey, 2 grey + alpha, 3 RGB, 4 RGBA), rows top down,
# starting at px[at], written as a PNG at `path`
@alloc_ok("a bake's preview or a lab shot: one image written, once")
export function lab_png_write_from(lab_png_st: LabPngState, path: string, w: int, h: int, channels: int, px: []byte, at: int) -> bool {
if channels < 1 or channels > 4 or w <= 0 or h <= 0 or at + w * h * channels > len(px) { return false }
let out = buffer(lp_png_size(w, h, channels))
let n = lp_encode(lab_png_st.table, w, h, channels, px, at, out)
let ok = Fs.write_bytes(path, out, n)
free(out)
return ok
}
export function lab_png_write(lab_png_st: LabPngState, path: string, w: int, h: int, channels: int, px: []byte) -> bool {
return lab_png_write_from(lab_png_st, path, w, h, channels, px, 0)
}

View file

@ -1,4 +1,4 @@
# lab_test.ludic - what the lab computes without a window: the PNG it keeps a shot as, the cameras
# lab_test.ludic - what the lab computes without a window: the PNGs it writes (a shot, raw pixels), the cameras
# a scene names, and the scenes a program adds
import "ludic.lab"
program LabTest {
@ -35,6 +35,30 @@ program LabTest {
expect(not Fs.exists(`{dir}/lab_test.ppm`))
}
test "raw pixels become a PNG of their channel count" (lab_png_st: LabPngState) {
let dir = Os.temp_dir()
let px = buffer(3 * 2 * 4)
for i in 0 .. 24 { px[i] = i * 10 }
expect(lab_png_write(lab_png_st, `{dir}/lab_test_rgba.png`, 3, 2, 4, px))
let p = Fs.read_bytes(`{dir}/lab_test_rgba.png`)
expect_eq(p[19], 3) # width
expect_eq(p[25], 6) # colour type RGBA
expect_eq(len(p), lp_png_size(3, 2, 4))
expect(not lab_png_write(lab_png_st, `{dir}/lab_test_short.png`, 9, 9, 4, px))
}
test "a half float and a float read as the numbers they store" {
let b = buffer(8)
b[0] = 0; b[1] = 60 # half 1.0 = 0x3C00
b[2] = 0; b[3] = 192 # half -2.0 = 0xC000
b[4] = 0; b[5] = 0; b[6] = 128; b[7] = 63 # float 1.0 = 0x3F800000
expect_near(lab_f16_at(b, 0), 1.0, 0.0001)
expect_near(lab_f16_at(b, 2), -2.0, 0.0001)
expect_near(lab_f32_at(b, 4), 1.0, 0.0001)
let g = lab_px_rgba16f_hdr(b, 0, 1) # 1.0 -> 0.5 -> sRGB 188
expect_eq(g[0] & 255, 188)
}
test "a camera at a point looks back at it" (lab_st: mut LabState) {
lab_shots_reset(lab_st)
lab_shot_at(lab_st, "front", 0.0, 1.0, 0.0, 10.0, 0.0, 0.0)