feat: threads

This commit is contained in:
Dan Finch 2026-08-05 09:24:13 +02:00
parent 6fb46573da
commit 46e7070b6b
18 changed files with 878 additions and 125 deletions

View file

@ -40,13 +40,13 @@ export namespace Framebuffer {
* 15-bit output. Skipped entirely when it would be a no-op (full depth, no
* dither).
*/
export function quantize(fb: Framebuffer, config: RenderConfig): void {
export function quantize(fb: Framebuffer, config: RenderConfig, y0 = 0, y1 = fb.height): void {
const levels = (1 << config.colorDepth) - 1
if (levels >= 255 && config.dither === 0) {
return
}
const { width, height, color } = fb
for (let y = 0; y < height; y++) {
const { width, color } = fb
for (let y = y0; y < y1; y++) {
for (let x = 0; x < width; x++) {
// Per-pixel threshold from the tiled Bayer matrix, centered on 0 and
// scaled by strength, nudges each channel before it snaps to a level.

View file

@ -51,6 +51,8 @@ export namespace Rasterizer {
viewProj: Mat4,
config: RenderConfig,
cull = false,
clipY0 = 0,
clipY1 = 1 << 30,
): void {
const { verts, indices } = mesh
const flat = config.lighting === "flat"
@ -65,7 +67,7 @@ export namespace Rasterizer {
// Near-clipping can turn one triangle into a quad; fan it back to tris.
const n = clipNear(3)
for (let k = 1; k + 1 < n; k++) {
fillTriangle(fb, 0, k, k + 1, shade, texture, config, cull)
fillTriangle(fb, 0, k, k + 1, shade, texture, config, cull, clipY0, clipY1)
}
}
}
@ -167,6 +169,8 @@ export namespace Rasterizer {
texture: Texture,
config: RenderConfig,
cull: boolean,
clipY0: number,
clipY1: number,
): void {
const oa = ia * CLIP
const ob = ib * CLIP
@ -210,8 +214,10 @@ export namespace Rasterizer {
const vC = dst[oc + 4]
const minX = Math.max(0, Math.floor(Math.min(sxA, sxB, sxC)))
const maxX = Math.min(width - 1, Math.ceil(Math.max(sxA, sxB, sxC)))
const minY = Math.max(0, Math.floor(Math.min(syA, syB, syC)))
const maxY = Math.min(height - 1, Math.ceil(Math.max(syA, syB, syC)))
// Clamp to the caller's Y-band (default full frame) so worker threads can
// each fill a disjoint slice of rows without ever touching the same pixel.
const minY = Math.max(0, clipY0, Math.floor(Math.min(syA, syB, syC)))
const maxY = Math.min(height - 1, clipY1 - 1, Math.ceil(Math.max(syA, syB, syC)))
// Edge deltas for the three barycentric edge functions (b->c, c->a, a->b).
const dx0 = sxC - sxB
const dy0 = syC - syB

View file

@ -46,6 +46,11 @@ export type RenderConfig = {
* short draw distance and the shimmer of far geometry. It also colors pixels
* no triangle covers, so the frame's clear color should match `fog.color`. */
fog: Fog | null
/** Beyond this distance (world units) trees + boulders draw as cheap low-poly
* impostors instead of full geometry, cutting per-triangle work in dense
* views. Kept inside `fog.far` so far detail is already fog-dimmed at the
* switch; `Infinity` disables LOD. */
lodDistance: number
}
/** Ready-made looks. The demo binds keys 1/2/3 to these, sweeping resolution,
@ -61,6 +66,7 @@ export namespace RenderConfig {
textureFilter: "nearest",
lighting: "flat",
fog: { color: Color.rgb(150, 170, 200), near: 12, far: 200 },
lodDistance: 60,
}
export const ps1: RenderConfig = {
@ -73,6 +79,7 @@ export namespace RenderConfig {
textureFilter: "nearest",
lighting: "flat",
fog: { color: Color.rgb(150, 170, 200), near: 12, far: 200 },
lodDistance: 60,
}
export const soft: RenderConfig = {
@ -85,6 +92,7 @@ export namespace RenderConfig {
textureFilter: "nearest",
lighting: "flat",
fog: { color: Color.rgb(170, 190, 215), near: 16, far: 240 },
lodDistance: 70,
}
export const clean: RenderConfig = {
@ -94,9 +102,9 @@ export namespace RenderConfig {
colorDepth: 8,
dither: 0,
vertexSnap: 0,
perspectiveCorrect: 1,
textureFilter: "linear",
lighting: "flat",
fog: null,
lodDistance: Infinity,
}
}

View file

@ -64,8 +64,9 @@ export namespace Sky {
* block and is copied across it. The sky is low-frequency, so 2 is nearly free
* visually and quarters the cloud cost; 1 is full resolution.
*/
export function render(fb: Framebuffer, camera: Camera, sky: SkyConfig, time: number, step = 1): void {
export function render(fb: Framebuffer, camera: Camera, sky: SkyConfig, time: number, step = 1, y0 = 0, y1 = -1): void {
const { width, height, color, depth } = fb
const bottom = y1 < 0 ? height : y1
const forward = Camera.forward(camera)
const right = Vec3.normalize(Vec3.cross(forward, UP))
const up = Vec3.cross(right, forward)
@ -76,11 +77,13 @@ export namespace Sky {
const clouds = sky.clouds
const cloud: CloudSample = { cover: 0, shade: 1 }
const s = Math.max(1, step | 0)
for (let by = 0; by < height; by += s) {
// Band `y0`..`bottom` must be step-aligned (callers ensure it) so the block
// grid stays global and neighboring bands don't seam.
for (let by = y0; by < bottom; by += s) {
// Shade at the block center, then flood the whole block with that color.
const sampleY = Math.min(height - 1, by + (s >> 1))
const ndcY = 1 - ((sampleY + 0.5) / height) * 2
const yEnd = Math.min(height, by + s)
const yEnd = Math.min(bottom, by + s)
for (let bx = 0; bx < width; bx += s) {
const sampleX = Math.min(width - 1, bx + (s >> 1))
const ndcX = ((sampleX + 0.5) / width) * 2 - 1