From a8af7f59f37913f6c237ba856d946c78f6bd70cd Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Wed, 21 Oct 2026 21:58:42 +0100 Subject: [PATCH 1/3] Ray tracer benchmark: memory models vs Rust tslang/benchmarks/raytrace holds the raytrace.ts test at 512x512 with a pixel checksum, an idiomatic Rust port, a Rust port that heap-allocates every object through Rc, and bench.ps1, which builds them all, checks the checksum and times them. README records the 2026-10-21 results. Co-Authored-By: Claude Opus 5.5 --- tslang/benchmarks/raytrace/.gitignore | 3 + tslang/benchmarks/raytrace/README.md | 46 +++ tslang/benchmarks/raytrace/bench.ps1 | 41 +++ tslang/benchmarks/raytrace/rtbench.ts | 318 ++++++++++++++++++ tslang/benchmarks/raytrace/rust/Cargo.toml | 11 + .../benchmarks/raytrace/rust/src/bin/boxed.rs | 259 ++++++++++++++ .../raytrace/rust/src/bin/idiomatic.rs | 241 +++++++++++++ 7 files changed, 919 insertions(+) create mode 100644 tslang/benchmarks/raytrace/.gitignore create mode 100644 tslang/benchmarks/raytrace/README.md create mode 100644 tslang/benchmarks/raytrace/bench.ps1 create mode 100644 tslang/benchmarks/raytrace/rtbench.ts create mode 100644 tslang/benchmarks/raytrace/rust/Cargo.toml create mode 100644 tslang/benchmarks/raytrace/rust/src/bin/boxed.rs create mode 100644 tslang/benchmarks/raytrace/rust/src/bin/idiomatic.rs diff --git a/tslang/benchmarks/raytrace/.gitignore b/tslang/benchmarks/raytrace/.gitignore new file mode 100644 index 000000000..89d60790b --- /dev/null +++ b/tslang/benchmarks/raytrace/.gitignore @@ -0,0 +1,3 @@ +out/ +rust/target/ +rust/Cargo.lock diff --git a/tslang/benchmarks/raytrace/README.md b/tslang/benchmarks/raytrace/README.md new file mode 100644 index 000000000..8ffeb25b3 --- /dev/null +++ b/tslang/benchmarks/raytrace/README.md @@ -0,0 +1,46 @@ +# Ray tracer: memory models vs Rust + +`rtbench.ts` is `test/tester/tests/raytrace.ts` at 512x512 with a pixel checksum (see its header). +`rust/` holds two ports of the same algorithm: + +- `idiomatic.rs`: `Vector`/`Color` are `Copy` values, things are `Box`, surfaces hold + `fn` pointers. This is what a Rust programmer would write. +- `boxed.rs`: every `Vector`, `Color`, `Ray` and `Intersection` is a fresh `Rc` heap object, things + are `Rc`, surface members are `Rc` closures. This is roughly the allocation + pattern tslang produces, so the gap between the two ports is the cost of allocation alone. + +Run (needs a Release tslang and cargo): + +``` +pwsh tslang/benchmarks/raytrace/bench.ps1 +``` + +tslang builds use `--emit=exe --opt --opt_level=3 --no-default-lib --mm=` +(`--no-default-lib` because the file declares its own `sqrt`/`floor`/`pow`/`fabs`); Rust uses +`opt-level=3`, `lto="fat"`, `codegen-units=1`, `panic="abort"`. + +## Results 2026-10-21 + +main 49b5f6bb, Release, Windows 11 x64, Rust 1.99.0; 7 runs after a warm-up. + +| Build | median | peak working set | +|---|---|---| +| tslang gc | 1276 ms | 6 MB | +| tslang gc, `--no-stack-promotion` | 1335 ms | | +| tslang rc | 624 ms | 4 MB | +| tslang rc, `--no-stack-promotion` | 617 ms | | +| tslang none | 690 ms | 845 MB | +| tslang none, `--no-stack-promotion` | 716 ms | | +| tslang own | does not compile | | +| Rust idiomatic | 56 ms | 4 MB | +| Rust Rc-boxed | 504 ms | 4 MB | + +- Almost all of the gap to Rust is allocation: the Rc-boxed port is 9x slower than the idiomatic + one, and rc is 1.24x slower than the Rc-boxed port. +- `none` never frees, so it pays for fresh pages and ends up slower than `rc`. +- Stack promotion has no measurable effect here. `--emit=mlir-affine --stack-promotion-report` + under rc keeps every hot-path allocation on the heap: `Vector.*`/`Color.*` results are + "returned", the `Ray`/`Intersection` literals are "converted" (to the interface) or + "recursive", and the `addLight`/`getPoint` closures are "counted-capture". +- `-mm=own` reports about 44 errors: the program shares the `Surfaces` globals, keeps owned values + in object literals, and aliases `Color.black` from two statics. diff --git a/tslang/benchmarks/raytrace/bench.ps1 b/tslang/benchmarks/raytrace/bench.ps1 new file mode 100644 index 000000000..260415b60 --- /dev/null +++ b/tslang/benchmarks/raytrace/bench.ps1 @@ -0,0 +1,41 @@ +# Builds rtbench.ts under every memory model (with and without stack promotion) and the two Rust +# ports, checks that they all print the same checksum, then times each one: a warm-up run and +# $Runs timed runs, reporting min/median/max wall time. +# +# pwsh tslang/benchmarks/raytrace/bench.ps1 [-Tslang ] [-Runs 7] +param( + [string]$Tslang = "$PSScriptRoot\..\..\..\__build\tslang\windows-msbuild-2026-release\bin\tslang.exe", + [int]$Runs = 7 +) + +$ErrorActionPreference = "Stop" +$out = Join-Path $PSScriptRoot "out" +New-Item -ItemType Directory -Force $out | Out-Null + +$bins = @() +foreach ($mm in "gc", "rc", "none", "own") { + foreach ($sp in $false, $true) { + $name = if ($sp) { "tslang $mm (no SP)" } else { "tslang $mm" } + $exe = Join-Path $out ("rtb_$mm" + $(if ($sp) { "_nsp" } else { "" }) + ".exe") + $flags = @("--emit=exe", "--opt", "--opt_level=3", "--no-default-lib", "--mm=$mm") + if ($sp) { $flags += "--no-stack-promotion" } + & $Tslang @flags "$PSScriptRoot\rtbench.ts" -o $exe 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { "{0,-22} does not compile (exit {1})" -f $name, $LASTEXITCODE; continue } + $bins += @{ n = $name; p = $exe } + } +} + +Push-Location "$PSScriptRoot\rust" +try { cargo build --release --quiet; if ($LASTEXITCODE -ne 0) { throw "cargo build failed" } } finally { Pop-Location } +$bins += @{ n = "rust idiomatic"; p = "$PSScriptRoot\rust\target\release\idiomatic.exe" } +$bins += @{ n = "rust Rc-boxed"; p = "$PSScriptRoot\rust\target\release\boxed.exe" } + +foreach ($b in $bins) { + $text = (& $b.p) -join " " + $ok = $text -match "38837369|low digits: 369" + $times = 1..$Runs | ForEach-Object { + $sw = [Diagnostics.Stopwatch]::StartNew(); & $b.p | Out-Null; $sw.Stop(); $sw.Elapsed.TotalMilliseconds + } | Sort-Object + "{0,-22} min {1,8:N1} ms median {2,8:N1} ms max {3,8:N1} ms checksum {4}" -f ` + $b.n, $times[0], $times[[int][math]::Floor($Runs / 2)], $times[-1], $(if ($ok) { "ok" } else { "MISMATCH: $text" }) +} diff --git a/tslang/benchmarks/raytrace/rtbench.ts b/tslang/benchmarks/raytrace/rtbench.ts new file mode 100644 index 000000000..038398113 --- /dev/null +++ b/tslang/benchmarks/raytrace/rtbench.ts @@ -0,0 +1,318 @@ +// @strict-null false +// Benchmark copy of test/tester/tests/raytrace.ts: renders 512x512 instead of 256x256 and sums +// every pixel's drawing color into a checksum (38837369), so the render cannot be optimized away +// and every build, including the Rust ports in rust/, can be checked against the others. +// print() shows the sum with 6 significant digits, so the low digits are printed separately. +declare function sqrt(v: number): number; +declare function floor(v: number): number; +declare function pow(v: number, s: number): number; +declare function fabs(v: number): number; + +class Math { + static sqrt(v: number): number { + return sqrt(v); + } + + static floor(v: number): number { + return floor(v); + } + + static pow(v: number, s: number): number { + return pow(v, s); + } + + static abs(v: number): number { + return fabs(v); + } + + static sign(v: number): number { + if (v > 0) return 1; + if (v < 0) return -1; + return 0; + } +} + +class Vector { + constructor(public x: number, + public y: number, + public z: number) { + } + static times(k: number, v: Vector) { return new Vector(k * v.x, k * v.y, k * v.z); } + static minus(v1: Vector, v2: Vector) { return new Vector(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z); } + static plus(v1: Vector, v2: Vector) { return new Vector(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z); } + static dot(v1: Vector, v2: Vector) { return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z; } + static mag(v: Vector) { return Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z); } + static norm(v: Vector) { + let mag = Vector.mag(v); + let div = (mag === 0) ? Infinity : 1.0 / mag; + return Vector.times(div, v); + } + static cross(v1: Vector, v2: Vector) { + return new Vector(v1.y * v2.z - v1.z * v2.y, + v1.z * v2.x - v1.x * v2.z, + v1.x * v2.y - v1.y * v2.x); + } +} + +class Color { + constructor(public r: number, + public g: number, + public b: number) { + } + static scale(k: number, v: Color) { return new Color(k * v.r, k * v.g, k * v.b); } + static plus(v1: Color, v2: Color) { return new Color(v1.r + v2.r, v1.g + v2.g, v1.b + v2.b); } + static times(v1: Color, v2: Color) { return new Color(v1.r * v2.r, v1.g * v2.g, v1.b * v2.b); } + static white = new Color(1.0, 1.0, 1.0); + static grey = new Color(0.5, 0.5, 0.5); + static black = new Color(0.0, 0.0, 0.0); + static background = Color.black; + static defaultColor = Color.black; + static toDrawingColor(c: Color) { + let legalize = (d: number) => d > 1 ? 1 : d; + return { + r: Math.floor(legalize(c.r) * 255), + g: Math.floor(legalize(c.g) * 255), + b: Math.floor(legalize(c.b) * 255) + } + } +} + +class Camera { + public forward: Vector; + public right: Vector; + public up: Vector; + + constructor(public pos: Vector, lookAt: Vector) { + let down = new Vector(0.0, -1.0, 0.0); + this.forward = Vector.norm(Vector.minus(lookAt, this.pos)); + this.right = Vector.times(1.5, Vector.norm(Vector.cross(this.forward, down))); + this.up = Vector.times(1.5, Vector.norm(Vector.cross(this.forward, this.right))); + } +} + +interface Ray { + start: Vector; + dir: Vector; +} + +interface Intersection { + thing: Thing; + ray: Ray; + dist: number; +} + +interface Surface { + diffuse: (pos: Vector) => Color; + specular: (pos: Vector) => Color; + reflect: (pos: Vector) => number; + roughness: number; +} + +interface Thing { + intersect(ray: Ray): Intersection; + normal(pos: Vector): Vector; + surface: Surface; +} + +interface Light { + pos: Vector; + color: Color; +} + +interface Scene { + things: Thing[]; + lights: Light[]; + camera: Camera; +} + +class Sphere implements Thing { + public radius2: number; + + constructor(public center: Vector, radius: number, public surface: Surface) { + this.radius2 = radius * radius; + } + normal(pos: Vector): Vector { return Vector.norm(Vector.minus(pos, this.center)); } + intersect(ray: Ray): Intersection { + let eo = Vector.minus(this.center, ray.start); + let v = Vector.dot(eo, ray.dir); + let dist = 0.0; + if (v >= 0) { + let disc = this.radius2 - (Vector.dot(eo, eo) - v * v); + if (disc >= 0) { + dist = v - Math.sqrt(disc); + } + } + if (dist === 0) { + return null; + } else { + return { thing: this, ray: ray, dist: dist }; + } + } +} + +class Plane implements Thing { + constructor(public norm: Vector, public offset: number, public surface: Surface) { + } + public normal(pos: Vector): Vector { return this.norm; } + public intersect(ray: Ray): Intersection { + let denom = Vector.dot(this.norm, ray.dir); + if (denom > 0) { + return null; + } else { + let dist = (Vector.dot(this.norm, ray.start) + this.offset) / (-denom); + return { thing: this, ray: ray, dist: dist }; + } + } +} + +module Surfaces { + export let shiny: Surface = { + diffuse: function (pos: Vector) { return Color.white; }, + specular: function (pos: Vector) { return Color.grey; }, + reflect: function (pos: Vector) { return 0.7; }, + roughness: 250.0 + } + export let checkerboard: Surface = { + diffuse: function (pos: Vector) { + if ((Math.floor(pos.z) + Math.floor(pos.x)) % 2 !== 0) { + return Color.white; + } else { + return Color.black; + } + }, + specular: function (pos: Vector) { return Color.white; }, + reflect: function (pos: Vector) { + if ((Math.floor(pos.z) + Math.floor(pos.x)) % 2 !== 0) { + return 0.1; + } else { + return 0.7; + } + }, + roughness: 150.0 + } +} + + +class RayTracer { + private maxDepth = 5; + + private intersections(ray: Ray, scene: Scene) { + let closest = +Infinity; + let closestInter: Intersection = undefined; + for (let i in scene.things) { + let inter = scene.things[i].intersect(ray); + if (inter != null && inter.dist < closest) { + closestInter = inter; + closest = inter.dist; + } + } + + return closestInter; + } + + private testRay(ray: Ray, scene: Scene) { + let isect = this.intersections(ray, scene); + if (isect != null) { + return isect.dist; + } else { + return undefined; + } + } + + private traceRay(ray: Ray, scene: Scene, depth: number): Color { + let isect = this.intersections(ray, scene); + if (isect === undefined) { + return Color.background; + } else { + return this.shade(isect, scene, depth); + } + } + + private shade(isect: Intersection, scene: Scene, depth: number) { + let d = isect.ray.dir; + let pos = Vector.plus(Vector.times(isect.dist, d), isect.ray.start); + let normal = isect.thing.normal(pos); + let reflectDir = Vector.minus(d, Vector.times(2, Vector.times(Vector.dot(normal, d), normal))); + let naturalColor = Color.plus(Color.background, + this.getNaturalColor(isect.thing, pos, normal, reflectDir, scene)); + let reflectedColor = (depth >= this.maxDepth) ? Color.grey : this.getReflectionColor(isect.thing, pos, normal, reflectDir, scene, depth); + return Color.plus(naturalColor, reflectedColor); + } + + private getReflectionColor(thing: Thing, pos: Vector, normal: Vector, rd: Vector, scene: Scene, depth: number) { + return Color.scale(thing.surface.reflect(pos), this.traceRay({ start: pos, dir: rd }, scene, depth + 1)); + } + + private getNaturalColor(thing: Thing, pos: Vector, norm: Vector, rd: Vector, scene: Scene) { + const addLight = (col: Color, light: Light) => { + let ldis = Vector.minus(light.pos, pos); + let livec = Vector.norm(ldis); + let neatIsect = this.testRay({ start: pos, dir: livec }, scene); + let isInShadow = (neatIsect === undefined) ? false : (neatIsect <= Vector.mag(ldis)); + if (isInShadow) { + return col; + } else { + let illum = Vector.dot(livec, norm); + let lcolor = (illum > 0) ? Color.scale(illum, light.color) + : Color.defaultColor; + let specular = Vector.dot(livec, Vector.norm(rd)); + let scolor = (specular > 0) ? Color.scale(Math.pow(specular, thing.surface.roughness), light.color) + : Color.defaultColor; + return Color.plus(col, Color.plus(Color.times(thing.surface.diffuse(pos), lcolor), + Color.times(thing.surface.specular(pos), scolor))); + } + } + + return scene.lights.reduce(addLight, Color.defaultColor); + } + + render(scene: Scene, screenWidth: number, screenHeight: number) { + let sum: number = 0; + const getPoint = (x: number, y: number, camera: Camera) => { + const recenterX = (x: number) => (x - (screenWidth / 2.0)) / 2.0 / screenWidth; + const recenterY = (y: number) => - (y - (screenHeight / 2.0)) / 2.0 / screenHeight; + return Vector.norm(Vector.plus(camera.forward, Vector.plus(Vector.times(recenterX(x), camera.right), Vector.times(recenterY(y), camera.up)))); + } + + for (let y = 0; y < screenHeight; y++) { + for (let x = 0; x < screenWidth; x++) { + let color = this.traceRay({ start: scene.camera.pos, dir: getPoint(x, y, scene.camera) }, scene, 0); + let c = Color.toDrawingColor(color); + sum += c.r + c.g + c.b; + //ctx.fillStyle = "rgb(" + String(c.r) + ", " + String(c.g) + ", " + String(c.b) + ")"; + //ctx.fillRect(x, y, x + 1, y + 1); + } + } + return sum; + } +} + + +function defaultScene(): Scene { + return { + things: [new Plane(new Vector(0.0, 1.0, 0.0), 0.0, Surfaces.checkerboard), + new Sphere(new Vector(0.0, 1.0, -0.25), 1.0, Surfaces.shiny), + new Sphere(new Vector(-6, 3.0, -5.0), 1.0, Surfaces.shiny), + // new Sphere(new Vector(-6, 8.0, 15.0), 5.0, Surfaces.shiny), + new Sphere(new Vector(-1.0, 0.5, 1.5), 0.5, Surfaces.shiny)], + + lights: [{ pos: new Vector(-2.0, 2.5, 0.0), color: new Color(0.49, 0.07, 0.07) }, + { pos: new Vector(1.5, 2.5, 1.5), color: new Color(0.07, 0.07, 0.49) }, + { pos: new Vector(1.5, 2.5, -1.5), color: new Color(0.07, 0.49, 0.071) }, + { pos: new Vector(0.0, 3.5, 0.0), color: new Color(0.21, 0.21, 0.35) }], + camera: new Camera(new Vector(3.0, 2.0, 4.0), new Vector(-1.0, 0.5, 0.0)) + }; +} + +function exec() { + let rayTracer = new RayTracer(); + let sum = rayTracer.render(defaultScene(), 512, 512); + print("checksum:", sum, "low digits:", sum - 38837000); +} + + +function main() { + print("start..."); + exec(); + print("done."); +} diff --git a/tslang/benchmarks/raytrace/rust/Cargo.toml b/tslang/benchmarks/raytrace/rust/Cargo.toml new file mode 100644 index 000000000..d0e85fcb4 --- /dev/null +++ b/tslang/benchmarks/raytrace/rust/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "rt" +version = "0.1.0" +edition = "2021" + +[profile.release] +opt-level = 3 +codegen-units = 1 +lto = "fat" +panic = "abort" + diff --git a/tslang/benchmarks/raytrace/rust/src/bin/boxed.rs b/tslang/benchmarks/raytrace/rust/src/bin/boxed.rs new file mode 100644 index 000000000..63ee5fd25 --- /dev/null +++ b/tslang/benchmarks/raytrace/rust/src/bin/boxed.rs @@ -0,0 +1,259 @@ +// Rust port of raytrace.ts that keeps the TypeScript allocation pattern: every Vector, Color, +// Ray and Intersection is a fresh reference-counted heap object (Rc), things are Rc, +// surface members are Rc closures. Same algorithm as idiomatic.rs. + +use std::rc::Rc; + +struct Vector { x: f64, y: f64, z: f64 } +type V = Rc; + +fn vec3(x: f64, y: f64, z: f64) -> V { Rc::new(Vector { x, y, z }) } + +impl Vector { + fn times(k: f64, v: &V) -> V { vec3(k * v.x, k * v.y, k * v.z) } + fn minus(a: &V, b: &V) -> V { vec3(a.x - b.x, a.y - b.y, a.z - b.z) } + fn plus(a: &V, b: &V) -> V { vec3(a.x + b.x, a.y + b.y, a.z + b.z) } + fn dot(a: &V, b: &V) -> f64 { a.x * b.x + a.y * b.y + a.z * b.z } + fn mag(v: &V) -> f64 { (v.x * v.x + v.y * v.y + v.z * v.z).sqrt() } + fn norm(v: &V) -> V { + let mag = Vector::mag(v); + let div = if mag == 0.0 { f64::INFINITY } else { 1.0 / mag }; + Vector::times(div, v) + } + fn cross(a: &V, b: &V) -> V { + vec3(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x) + } +} + +struct Color { r: f64, g: f64, b: f64 } +type C = Rc; + +fn rgb(r: f64, g: f64, b: f64) -> C { Rc::new(Color { r, g, b }) } + +struct Colors { white: C, grey: C, black: C, background: C, default_color: C } + +impl Color { + fn scale(k: f64, v: &C) -> C { rgb(k * v.r, k * v.g, k * v.b) } + fn plus(a: &C, b: &C) -> C { rgb(a.r + b.r, a.g + b.g, a.b + b.b) } + fn times(a: &C, b: &C) -> C { rgb(a.r * b.r, a.g * b.g, a.b * b.b) } + fn to_drawing_color(c: &C) -> Rc<(f64, f64, f64)> { + let legalize = |d: f64| if d > 1.0 { 1.0 } else { d }; + Rc::new(((legalize(c.r) * 255.0).floor(), (legalize(c.g) * 255.0).floor(), (legalize(c.b) * 255.0).floor())) + } +} + +struct Camera { pos: V, forward: V, right: V, up: V } + +impl Camera { + fn new(pos: V, look_at: V) -> Rc { + let down = vec3(0.0, -1.0, 0.0); + let forward = Vector::norm(&Vector::minus(&look_at, &pos)); + let right = Vector::times(1.5, &Vector::norm(&Vector::cross(&forward, &down))); + let up = Vector::times(1.5, &Vector::norm(&Vector::cross(&forward, &right))); + Rc::new(Camera { pos, forward, right, up }) + } +} + +struct Ray { start: V, dir: V } +struct Intersection { thing: Rc, ray: Rc, dist: f64 } + +struct Surface { + diffuse: Rc C>, + specular: Rc C>, + reflect: Rc f64>, + roughness: f64, +} + +trait Thing { + fn intersect(self: Rc, ray: &Rc) -> Option>; + fn normal(&self, pos: &V) -> V; + fn surface(&self) -> &Rc; +} + +struct Light { pos: V, color: C } +struct Scene { things: Vec>, lights: Vec>, camera: Rc } + +struct Sphere { center: V, radius2: f64, surface: Rc } + +impl Thing for Sphere { + fn normal(&self, pos: &V) -> V { Vector::norm(&Vector::minus(pos, &self.center)) } + fn intersect(self: Rc, ray: &Rc) -> Option> { + let eo = Vector::minus(&self.center, &ray.start); + let v = Vector::dot(&eo, &ray.dir); + let mut dist = 0.0; + if v >= 0.0 { + let disc = self.radius2 - (Vector::dot(&eo, &eo) - v * v); + if disc >= 0.0 { dist = v - disc.sqrt(); } + } + if dist == 0.0 { None } else { Some(Rc::new(Intersection { thing: self, ray: ray.clone(), dist })) } + } + fn surface(&self) -> &Rc { &self.surface } +} + +struct Plane { norm: V, offset: f64, surface: Rc } + +impl Thing for Plane { + fn normal(&self, _pos: &V) -> V { self.norm.clone() } + fn intersect(self: Rc, ray: &Rc) -> Option> { + let denom = Vector::dot(&self.norm, &ray.dir); + if denom > 0.0 { + None + } else { + let dist = (Vector::dot(&self.norm, &ray.start) + self.offset) / (-denom); + Some(Rc::new(Intersection { thing: self, ray: ray.clone(), dist })) + } + } + fn surface(&self) -> &Rc { &self.surface } +} + +struct RayTracer { max_depth: i32, colors: Rc } + +impl RayTracer { + fn intersections(&self, ray: &Rc, scene: &Scene) -> Option> { + let mut closest = f64::INFINITY; + let mut closest_inter = None; + for thing in &scene.things { + if let Some(inter) = thing.clone().intersect(ray) { + if inter.dist < closest { + closest = inter.dist; + closest_inter = Some(inter); + } + } + } + closest_inter + } + + fn test_ray(&self, ray: &Rc, scene: &Scene) -> Option { + self.intersections(ray, scene).map(|i| i.dist) + } + + fn trace_ray(&self, ray: &Rc, scene: &Scene, depth: i32) -> C { + match self.intersections(ray, scene) { + None => self.colors.background.clone(), + Some(isect) => self.shade(&isect, scene, depth), + } + } + + fn shade(&self, isect: &Rc, scene: &Scene, depth: i32) -> C { + let d = isect.ray.dir.clone(); + let pos = Vector::plus(&Vector::times(isect.dist, &d), &isect.ray.start); + let normal = isect.thing.normal(&pos); + let reflect_dir = Vector::minus(&d, &Vector::times(2.0, &Vector::times(Vector::dot(&normal, &d), &normal))); + let natural_color = Color::plus(&self.colors.background, + &self.get_natural_color(&isect.thing, &pos, &normal, &reflect_dir, scene)); + let reflected_color = if depth >= self.max_depth { + self.colors.grey.clone() + } else { + self.get_reflection_color(&isect.thing, &pos, &normal, &reflect_dir, scene, depth) + }; + Color::plus(&natural_color, &reflected_color) + } + + fn get_reflection_color(&self, thing: &Rc, pos: &V, _normal: &V, rd: &V, scene: &Scene, depth: i32) -> C { + let ray = Rc::new(Ray { start: pos.clone(), dir: rd.clone() }); + Color::scale((thing.surface().reflect)(pos), &self.trace_ray(&ray, scene, depth + 1)) + } + + fn get_natural_color(&self, thing: &Rc, pos: &V, norm: &V, rd: &V, scene: &Scene) -> C { + let add_light = |col: C, light: &Rc| -> C { + let ldis = Vector::minus(&light.pos, pos); + let livec = Vector::norm(&ldis); + let neat_isect = self.test_ray(&Rc::new(Ray { start: pos.clone(), dir: livec.clone() }), scene); + let is_in_shadow = match neat_isect { None => false, Some(d) => d <= Vector::mag(&ldis) }; + if is_in_shadow { + col + } else { + let illum = Vector::dot(&livec, norm); + let lcolor = if illum > 0.0 { Color::scale(illum, &light.color) } else { self.colors.default_color.clone() }; + let specular = Vector::dot(&livec, &Vector::norm(rd)); + let scolor = if specular > 0.0 { + Color::scale(specular.powf(thing.surface().roughness), &light.color) + } else { + self.colors.default_color.clone() + }; + Color::plus(&col, &Color::plus(&Color::times(&(thing.surface().diffuse)(pos), &lcolor), + &Color::times(&(thing.surface().specular)(pos), &scolor))) + } + }; + scene.lights.iter().fold(self.colors.default_color.clone(), add_light) + } + + fn render(&self, scene: &Scene, screen_width: f64, screen_height: f64) -> f64 { + let get_point = |x: f64, y: f64, camera: &Rc| { + let recenter_x = |x: f64| (x - (screen_width / 2.0)) / 2.0 / screen_width; + let recenter_y = |y: f64| -(y - (screen_height / 2.0)) / 2.0 / screen_height; + Vector::norm(&Vector::plus(&camera.forward, &Vector::plus(&Vector::times(recenter_x(x), &camera.right), &Vector::times(recenter_y(y), &camera.up)))) + }; + let mut sum = 0.0; + let mut y = 0.0; + while y < screen_height { + let mut x = 0.0; + while x < screen_width { + let ray = Rc::new(Ray { start: scene.camera.pos.clone(), dir: get_point(x, y, &scene.camera) }); + let color = self.trace_ray(&ray, scene, 0); + let c = Color::to_drawing_color(&color); + sum += c.0 + c.1 + c.2; + x += 1.0; + } + y += 1.0; + } + sum + } +} + +fn surfaces(colors: &Rc) -> (Rc, Rc) { + let (w, g) = (colors.white.clone(), colors.grey.clone()); + let w2 = colors.white.clone(); + let shiny = Rc::new(Surface { + diffuse: Rc::new(move |_| w.clone()), + specular: Rc::new(move |_| g.clone()), + reflect: Rc::new(|_| 0.7), + roughness: 250.0, + }); + let (w3, b3) = (colors.white.clone(), colors.black.clone()); + let checkerboard = Rc::new(Surface { + diffuse: Rc::new(move |pos: &V| if (pos.z.floor() + pos.x.floor()) % 2.0 != 0.0 { w3.clone() } else { b3.clone() }), + specular: Rc::new(move |_| w2.clone()), + reflect: Rc::new(|pos: &V| if (pos.z.floor() + pos.x.floor()) % 2.0 != 0.0 { 0.1 } else { 0.7 }), + roughness: 150.0, + }); + (shiny, checkerboard) +} + +fn default_scene(colors: &Rc) -> Scene { + let (shiny, checkerboard) = surfaces(colors); + let sphere = |c: V, r: f64, s: &Rc| -> Rc { Rc::new(Sphere { center: c, radius2: r * r, surface: s.clone() }) }; + let light = |pos: V, color: C| Rc::new(Light { pos, color }); + Scene { + things: vec![ + Rc::new(Plane { norm: vec3(0.0, 1.0, 0.0), offset: 0.0, surface: checkerboard }), + sphere(vec3(0.0, 1.0, -0.25), 1.0, &shiny), + sphere(vec3(-6.0, 3.0, -5.0), 1.0, &shiny), + sphere(vec3(-1.0, 0.5, 1.5), 0.5, &shiny), + ], + lights: vec![ + light(vec3(-2.0, 2.5, 0.0), rgb(0.49, 0.07, 0.07)), + light(vec3(1.5, 2.5, 1.5), rgb(0.07, 0.07, 0.49)), + light(vec3(1.5, 2.5, -1.5), rgb(0.07, 0.49, 0.071)), + light(vec3(0.0, 3.5, 0.0), rgb(0.21, 0.21, 0.35)), + ], + camera: Camera::new(vec3(3.0, 2.0, 4.0), vec3(-1.0, 0.5, 0.0)), + } +} + +fn main() { + println!("start..."); + let black = rgb(0.0, 0.0, 0.0); + let colors = Rc::new(Colors { + white: rgb(1.0, 1.0, 1.0), + grey: rgb(0.5, 0.5, 0.5), + background: black.clone(), + default_color: black.clone(), + black, + }); + let ray_tracer = RayTracer { max_depth: 5, colors: colors.clone() }; + let size: f64 = std::env::args().nth(1).and_then(|s| s.parse().ok()).unwrap_or(512.0); + let sum = ray_tracer.render(&default_scene(&colors), size, size); + println!("checksum: {}", sum); + println!("done."); +} diff --git a/tslang/benchmarks/raytrace/rust/src/bin/idiomatic.rs b/tslang/benchmarks/raytrace/rust/src/bin/idiomatic.rs new file mode 100644 index 000000000..c3eac7fed --- /dev/null +++ b/tslang/benchmarks/raytrace/rust/src/bin/idiomatic.rs @@ -0,0 +1,241 @@ +// Idiomatic Rust port of tslang/test/tester/tests/raytrace.ts: +// Vector/Color are Copy values, things are Box, surfaces hold fn pointers. + +#[derive(Clone, Copy)] +struct Vector { x: f64, y: f64, z: f64 } + +impl Vector { + fn new(x: f64, y: f64, z: f64) -> Self { Vector { x, y, z } } + fn times(k: f64, v: Vector) -> Vector { Vector::new(k * v.x, k * v.y, k * v.z) } + fn minus(a: Vector, b: Vector) -> Vector { Vector::new(a.x - b.x, a.y - b.y, a.z - b.z) } + fn plus(a: Vector, b: Vector) -> Vector { Vector::new(a.x + b.x, a.y + b.y, a.z + b.z) } + fn dot(a: Vector, b: Vector) -> f64 { a.x * b.x + a.y * b.y + a.z * b.z } + fn mag(v: Vector) -> f64 { (v.x * v.x + v.y * v.y + v.z * v.z).sqrt() } + fn norm(v: Vector) -> Vector { + let mag = Vector::mag(v); + let div = if mag == 0.0 { f64::INFINITY } else { 1.0 / mag }; + Vector::times(div, v) + } + fn cross(a: Vector, b: Vector) -> Vector { + Vector::new(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x) + } +} + +#[derive(Clone, Copy)] +struct Color { r: f64, g: f64, b: f64 } + +const WHITE: Color = Color { r: 1.0, g: 1.0, b: 1.0 }; +const GREY: Color = Color { r: 0.5, g: 0.5, b: 0.5 }; +const BLACK: Color = Color { r: 0.0, g: 0.0, b: 0.0 }; +const BACKGROUND: Color = BLACK; +const DEFAULT_COLOR: Color = BLACK; + +impl Color { + fn scale(k: f64, v: Color) -> Color { Color { r: k * v.r, g: k * v.g, b: k * v.b } } + fn plus(a: Color, b: Color) -> Color { Color { r: a.r + b.r, g: a.g + b.g, b: a.b + b.b } } + fn times(a: Color, b: Color) -> Color { Color { r: a.r * b.r, g: a.g * b.g, b: a.b * b.b } } + fn to_drawing_color(c: Color) -> (f64, f64, f64) { + let legalize = |d: f64| if d > 1.0 { 1.0 } else { d }; + ((legalize(c.r) * 255.0).floor(), (legalize(c.g) * 255.0).floor(), (legalize(c.b) * 255.0).floor()) + } +} + +struct Camera { pos: Vector, forward: Vector, right: Vector, up: Vector } + +impl Camera { + fn new(pos: Vector, look_at: Vector) -> Self { + let down = Vector::new(0.0, -1.0, 0.0); + let forward = Vector::norm(Vector::minus(look_at, pos)); + let right = Vector::times(1.5, Vector::norm(Vector::cross(forward, down))); + let up = Vector::times(1.5, Vector::norm(Vector::cross(forward, right))); + Camera { pos, forward, right, up } + } +} + +#[derive(Clone, Copy)] +struct Ray { start: Vector, dir: Vector } + +struct Intersection<'a> { thing: &'a dyn Thing, ray: Ray, dist: f64 } + +struct Surface { + diffuse: fn(Vector) -> Color, + specular: fn(Vector) -> Color, + reflect: fn(Vector) -> f64, + roughness: f64, +} + +trait Thing { + fn intersect(&self, ray: Ray) -> Option>; + fn normal(&self, pos: Vector) -> Vector; + fn surface(&self) -> &Surface; +} + +struct Light { pos: Vector, color: Color } + +struct Scene { things: Vec>, lights: Vec, camera: Camera } + +struct Sphere { center: Vector, radius2: f64, surface: &'static Surface } + +impl Thing for Sphere { + fn normal(&self, pos: Vector) -> Vector { Vector::norm(Vector::minus(pos, self.center)) } + fn intersect(&self, ray: Ray) -> Option> { + let eo = Vector::minus(self.center, ray.start); + let v = Vector::dot(eo, ray.dir); + let mut dist = 0.0; + if v >= 0.0 { + let disc = self.radius2 - (Vector::dot(eo, eo) - v * v); + if disc >= 0.0 { dist = v - disc.sqrt(); } + } + if dist == 0.0 { None } else { Some(Intersection { thing: self, ray, dist }) } + } + fn surface(&self) -> &Surface { self.surface } +} + +struct Plane { norm: Vector, offset: f64, surface: &'static Surface } + +impl Thing for Plane { + fn normal(&self, _pos: Vector) -> Vector { self.norm } + fn intersect(&self, ray: Ray) -> Option> { + let denom = Vector::dot(self.norm, ray.dir); + if denom > 0.0 { + None + } else { + let dist = (Vector::dot(self.norm, ray.start) + self.offset) / (-denom); + Some(Intersection { thing: self, ray, dist }) + } + } + fn surface(&self) -> &Surface { self.surface } +} + +static SHINY: Surface = Surface { + diffuse: |_| WHITE, + specular: |_| GREY, + reflect: |_| 0.7, + roughness: 250.0, +}; + +static CHECKERBOARD: Surface = Surface { + diffuse: |pos| if (pos.z.floor() + pos.x.floor()) % 2.0 != 0.0 { WHITE } else { BLACK }, + specular: |_| WHITE, + reflect: |pos| if (pos.z.floor() + pos.x.floor()) % 2.0 != 0.0 { 0.1 } else { 0.7 }, + roughness: 150.0, +}; + +struct RayTracer { max_depth: i32 } + +impl RayTracer { + fn intersections<'a>(&self, ray: Ray, scene: &'a Scene) -> Option> { + let mut closest = f64::INFINITY; + let mut closest_inter = None; + for thing in &scene.things { + if let Some(inter) = thing.intersect(ray) { + if inter.dist < closest { + closest = inter.dist; + closest_inter = Some(inter); + } + } + } + closest_inter + } + + fn test_ray(&self, ray: Ray, scene: &Scene) -> Option { + self.intersections(ray, scene).map(|i| i.dist) + } + + fn trace_ray(&self, ray: Ray, scene: &Scene, depth: i32) -> Color { + match self.intersections(ray, scene) { + None => BACKGROUND, + Some(isect) => self.shade(&isect, scene, depth), + } + } + + fn shade(&self, isect: &Intersection, scene: &Scene, depth: i32) -> Color { + let d = isect.ray.dir; + let pos = Vector::plus(Vector::times(isect.dist, d), isect.ray.start); + let normal = isect.thing.normal(pos); + let reflect_dir = Vector::minus(d, Vector::times(2.0, Vector::times(Vector::dot(normal, d), normal))); + let natural_color = Color::plus(BACKGROUND, self.get_natural_color(isect.thing, pos, normal, reflect_dir, scene)); + let reflected_color = if depth >= self.max_depth { + GREY + } else { + self.get_reflection_color(isect.thing, pos, normal, reflect_dir, scene, depth) + }; + Color::plus(natural_color, reflected_color) + } + + fn get_reflection_color(&self, thing: &dyn Thing, pos: Vector, _normal: Vector, rd: Vector, scene: &Scene, depth: i32) -> Color { + Color::scale((thing.surface().reflect)(pos), self.trace_ray(Ray { start: pos, dir: rd }, scene, depth + 1)) + } + + fn get_natural_color(&self, thing: &dyn Thing, pos: Vector, norm: Vector, rd: Vector, scene: &Scene) -> Color { + let add_light = |col: Color, light: &Light| { + let ldis = Vector::minus(light.pos, pos); + let livec = Vector::norm(ldis); + let neat_isect = self.test_ray(Ray { start: pos, dir: livec }, scene); + let is_in_shadow = match neat_isect { None => false, Some(d) => d <= Vector::mag(ldis) }; + if is_in_shadow { + col + } else { + let illum = Vector::dot(livec, norm); + let lcolor = if illum > 0.0 { Color::scale(illum, light.color) } else { DEFAULT_COLOR }; + let specular = Vector::dot(livec, Vector::norm(rd)); + let scolor = if specular > 0.0 { + Color::scale(specular.powf(thing.surface().roughness), light.color) + } else { + DEFAULT_COLOR + }; + Color::plus(col, Color::plus(Color::times((thing.surface().diffuse)(pos), lcolor), + Color::times((thing.surface().specular)(pos), scolor))) + } + }; + scene.lights.iter().fold(DEFAULT_COLOR, add_light) + } + + fn render(&self, scene: &Scene, screen_width: f64, screen_height: f64) -> f64 { + let get_point = |x: f64, y: f64, camera: &Camera| { + let recenter_x = |x: f64| (x - (screen_width / 2.0)) / 2.0 / screen_width; + let recenter_y = |y: f64| -(y - (screen_height / 2.0)) / 2.0 / screen_height; + Vector::norm(Vector::plus(camera.forward, Vector::plus(Vector::times(recenter_x(x), camera.right), Vector::times(recenter_y(y), camera.up)))) + }; + let mut sum = 0.0; + let mut y = 0.0; + while y < screen_height { + let mut x = 0.0; + while x < screen_width { + let color = self.trace_ray(Ray { start: scene.camera.pos, dir: get_point(x, y, &scene.camera) }, scene, 0); + let c = Color::to_drawing_color(color); + sum += c.0 + c.1 + c.2; + x += 1.0; + } + y += 1.0; + } + sum + } +} + +fn default_scene() -> Scene { + Scene { + things: vec![ + Box::new(Plane { norm: Vector::new(0.0, 1.0, 0.0), offset: 0.0, surface: &CHECKERBOARD }), + Box::new(Sphere { center: Vector::new(0.0, 1.0, -0.25), radius2: 1.0, surface: &SHINY }), + Box::new(Sphere { center: Vector::new(-6.0, 3.0, -5.0), radius2: 1.0, surface: &SHINY }), + Box::new(Sphere { center: Vector::new(-1.0, 0.5, 1.5), radius2: 0.25, surface: &SHINY }), + ], + lights: vec![ + Light { pos: Vector::new(-2.0, 2.5, 0.0), color: Color { r: 0.49, g: 0.07, b: 0.07 } }, + Light { pos: Vector::new(1.5, 2.5, 1.5), color: Color { r: 0.07, g: 0.07, b: 0.49 } }, + Light { pos: Vector::new(1.5, 2.5, -1.5), color: Color { r: 0.07, g: 0.49, b: 0.071 } }, + Light { pos: Vector::new(0.0, 3.5, 0.0), color: Color { r: 0.21, g: 0.21, b: 0.35 } }, + ], + camera: Camera::new(Vector::new(3.0, 2.0, 4.0), Vector::new(-1.0, 0.5, 0.0)), + } +} + +fn main() { + println!("start..."); + let ray_tracer = RayTracer { max_depth: 5 }; + let size: f64 = std::env::args().nth(1).and_then(|s| s.parse().ok()).unwrap_or(512.0); + let sum = ray_tracer.render(&default_scene(), size, size); + println!("checksum: {}", sum); + println!("done."); +} From 5de4db7aa3249e18b8cbeb2dbb741cbe6b11415d Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Wed, 21 Oct 2026 22:00:40 +0100 Subject: [PATCH 2/3] README: ray tracer results per memory model vs Rust Co-Authored-By: Claude Opus 5.5 --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index 9cb7649e3..89e6266cd 100644 --- a/README.md +++ b/README.md @@ -756,6 +756,25 @@ freed under it**, which is the same trade Swift makes with ARC. See **[docs/memory-models.md](docs/memory-models.md)** for which shapes leak, which do not, and what to do about it. +### Performance + +The ray tracer benchmark in [tslang/benchmarks/raytrace](tslang/benchmarks/raytrace/README.md) +(`raytrace.ts` at 512x512, median of 7 runs, 2026-10-21) against two Rust ports of the same code: + +| build | time | peak memory | vs idiomatic Rust | +| --- | --- | --- | --- | +| `-mm=gc` | 1276 ms | 6 MB | 23x slower | +| `-mm=rc` | 624 ms | 4 MB | 11x slower | +| `-mm=none` | 690 ms | 845 MB | 12x slower | +| `-mm=own` | does not compile yet | | | +| Rust, values (`Copy` vectors and colors) | 56 ms | 4 MB | | +| Rust, every object in an `Rc` | 504 ms | 4 MB | 9x slower | + +Almost all of the gap is allocation: every vector and color the program computes is a heap object, +and Rust that allocates the same way is only 1.24x faster than `-mm=rc`. `-mm=none` is slower than +`-mm=rc` because it never frees and keeps touching fresh pages. Stack promotion makes no measurable +difference here yet: the hot allocations are returned from functions or captured by closures. + ### `-mm=own` `-mm=own` works the way Rust's borrow checker does. Every heap block has exactly one owner: a From bc50d23aa0d7184da060afe914c1707c0cc2e215 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Wed, 21 Oct 2026 22:04:57 +0100 Subject: [PATCH 3/3] Ray tracer benchmark: add C++ ports cpp/idiomatic.cpp (value structs, unique_ptr things) and cpp/shared.cpp (every object a make_shared, std::function surfaces), built by bench.ps1 with clang++ -O3 and MSVC /O2. Both READMEs carry the new numbers. Co-Authored-By: Claude Opus 5.5 --- README.md | 25 +- tslang/benchmarks/raytrace/README.md | 64 +++-- tslang/benchmarks/raytrace/bench.ps1 | 17 ++ tslang/benchmarks/raytrace/cpp/idiomatic.cpp | 230 +++++++++++++++++ tslang/benchmarks/raytrace/cpp/shared.cpp | 245 +++++++++++++++++++ 5 files changed, 544 insertions(+), 37 deletions(-) create mode 100644 tslang/benchmarks/raytrace/cpp/idiomatic.cpp create mode 100644 tslang/benchmarks/raytrace/cpp/shared.cpp diff --git a/README.md b/README.md index 89e6266cd..7b881a956 100644 --- a/README.md +++ b/README.md @@ -759,19 +759,24 @@ what to do about it. ### Performance The ray tracer benchmark in [tslang/benchmarks/raytrace](tslang/benchmarks/raytrace/README.md) -(`raytrace.ts` at 512x512, median of 7 runs, 2026-10-21) against two Rust ports of the same code: +(`raytrace.ts` at 512x512, median of 7 runs, 2026-10-21) against Rust and C++ ports of the same +code, each written twice: with plain value vectors and colors, and with every object +reference-counted on the heap the way tslang allocates them: -| build | time | peak memory | vs idiomatic Rust | +| build | time | peak memory | vs C++ values (clang -O3) | | --- | --- | --- | --- | -| `-mm=gc` | 1276 ms | 6 MB | 23x slower | -| `-mm=rc` | 624 ms | 4 MB | 11x slower | -| `-mm=none` | 690 ms | 845 MB | 12x slower | +| `-mm=gc` | 1298 ms | 6 MB | 26x slower | +| `-mm=rc` | 599 ms | 4 MB | 12x slower | +| `-mm=none` | 703 ms | 845 MB | 14x slower | | `-mm=own` | does not compile yet | | | -| Rust, values (`Copy` vectors and colors) | 56 ms | 4 MB | | -| Rust, every object in an `Rc` | 504 ms | 4 MB | 9x slower | - -Almost all of the gap is allocation: every vector and color the program computes is a heap object, -and Rust that allocates the same way is only 1.24x faster than `-mm=rc`. `-mm=none` is slower than +| C++, values (clang / MSVC) | 51 / 69 ms | | | +| Rust, values (`Copy` vectors and colors) | 55 ms | 4 MB | 1.1x slower | +| Rust, every object in an `Rc` | 492 ms | 4 MB | 9.7x slower | +| C++, every object in a `shared_ptr` (clang / MSVC) | 830 / 899 ms | | 16x slower | + +Almost all of the gap is allocation: every vector and color the program computes is a heap object. +Rust that allocates the same way is only 1.2x faster than `-mm=rc`, and C++ with `shared_ptr` +(atomic counts) is 1.4x slower than it. `-mm=none` is slower than `-mm=rc` because it never frees and keeps touching fresh pages. Stack promotion makes no measurable difference here yet: the hot allocations are returned from functions or captured by closures. diff --git a/tslang/benchmarks/raytrace/README.md b/tslang/benchmarks/raytrace/README.md index 8ffeb25b3..8fe964426 100644 --- a/tslang/benchmarks/raytrace/README.md +++ b/tslang/benchmarks/raytrace/README.md @@ -1,42 +1,52 @@ -# Ray tracer: memory models vs Rust +# Ray tracer: memory models vs Rust and C++ `rtbench.ts` is `test/tester/tests/raytrace.ts` at 512x512 with a pixel checksum (see its header). -`rust/` holds two ports of the same algorithm: +`rust/` and `cpp/` each hold two ports of the same algorithm: -- `idiomatic.rs`: `Vector`/`Color` are `Copy` values, things are `Box`, surfaces hold - `fn` pointers. This is what a Rust programmer would write. -- `boxed.rs`: every `Vector`, `Color`, `Ray` and `Intersection` is a fresh `Rc` heap object, things - are `Rc`, surface members are `Rc` closures. This is roughly the allocation - pattern tslang produces, so the gap between the two ports is the cost of allocation alone. +- `rust/src/bin/idiomatic.rs`, `cpp/idiomatic.cpp`: `Vector`/`Color` are plain values, things are + `Box` / `std::unique_ptr`, surfaces hold function pointers. This is what a + Rust or C++ programmer would write. +- `rust/src/bin/boxed.rs`, `cpp/shared.cpp`: every `Vector`, `Color`, `Ray` and `Intersection` is + a fresh reference-counted heap object (`Rc` / `std::make_shared`), things are shared too, and + surface members are `Rc` / `std::function`. This is roughly the allocation pattern + tslang produces, so the gap between a language's two ports is the cost of allocation alone. + `Rc` counts are plain integers; `shared_ptr` counts are atomic. -Run (needs a Release tslang and cargo): +Run (needs a Release tslang and cargo; clang++ on PATH and Visual Studio are used when present): ``` pwsh tslang/benchmarks/raytrace/bench.ps1 ``` -tslang builds use `--emit=exe --opt --opt_level=3 --no-default-lib --mm=` -(`--no-default-lib` because the file declares its own `sqrt`/`floor`/`pow`/`fabs`); Rust uses -`opt-level=3`, `lto="fat"`, `codegen-units=1`, `panic="abort"`. +- tslang: `--emit=exe --opt --opt_level=3 --no-default-lib --mm=` (`--no-default-lib` + because the file declares its own `sqrt`/`floor`/`pow`/`fabs`) +- Rust: `opt-level=3`, `lto="fat"`, `codegen-units=1`, `panic="abort"` +- C++: `clang++ -std=c++20 -O3`, and `cl /std:c++20 /O2 /EHsc /MT` ## Results 2026-10-21 -main 49b5f6bb, Release, Windows 11 x64, Rust 1.99.0; 7 runs after a warm-up. - -| Build | median | peak working set | -|---|---|---| -| tslang gc | 1276 ms | 6 MB | -| tslang gc, `--no-stack-promotion` | 1335 ms | | -| tslang rc | 624 ms | 4 MB | -| tslang rc, `--no-stack-promotion` | 617 ms | | -| tslang none | 690 ms | 845 MB | -| tslang none, `--no-stack-promotion` | 716 ms | | -| tslang own | does not compile | | -| Rust idiomatic | 56 ms | 4 MB | -| Rust Rc-boxed | 504 ms | 4 MB | - -- Almost all of the gap to Rust is allocation: the Rc-boxed port is 9x slower than the idiomatic - one, and rc is 1.24x slower than the Rc-boxed port. +main 49b5f6bb, Release, Windows 11 x64; Rust 1.99.0, clang 21.1.4, MSVC from Visual Studio 18; +median of 7 runs after a warm-up. Every build prints the same checksum. + +| Build | median | peak working set | vs C++ idiomatic (clang) | +|---|---|---|---| +| tslang gc | 1298 ms | 6 MB | 26x | +| tslang gc, `--no-stack-promotion` | 1341 ms | | | +| tslang rc | 599 ms | 4 MB | 12x | +| tslang rc, `--no-stack-promotion` | 600 ms | | | +| tslang none | 703 ms | 845 MB | 14x | +| tslang none, `--no-stack-promotion` | 727 ms | | | +| tslang own | does not compile | | | +| Rust idiomatic | 55 ms | 4 MB | 1.1x | +| Rust Rc-boxed | 492 ms | 4 MB | 9.7x | +| C++ idiomatic (clang) | 51 ms | | 1x | +| C++ idiomatic (MSVC) | 69 ms | | 1.4x | +| C++ shared_ptr (clang) | 830 ms | | 16x | +| C++ shared_ptr (MSVC) | 899 ms | | 18x | + +- Almost all of the gap to Rust and C++ is allocation: their heap-allocating ports are 9-16x + slower than their idiomatic ones. rc is 1.2x slower than Rust with `Rc`, and 1.4x faster than + C++ with `shared_ptr` (atomic counts, `std::function` calls). - `none` never frees, so it pays for fresh pages and ends up slower than `rc`. - Stack promotion has no measurable effect here. `--emit=mlir-affine --stack-promotion-report` under rc keeps every hot-path allocation on the heap: `Vector.*`/`Color.*` results are diff --git a/tslang/benchmarks/raytrace/bench.ps1 b/tslang/benchmarks/raytrace/bench.ps1 index 260415b60..931408bcb 100644 --- a/tslang/benchmarks/raytrace/bench.ps1 +++ b/tslang/benchmarks/raytrace/bench.ps1 @@ -30,6 +30,23 @@ try { cargo build --release --quiet; if ($LASTEXITCODE -ne 0) { throw "cargo bui $bins += @{ n = "rust idiomatic"; p = "$PSScriptRoot\rust\target\release\idiomatic.exe" } $bins += @{ n = "rust Rc-boxed"; p = "$PSScriptRoot\rust\target\release\boxed.exe" } +# C++: clang++ -O3 from PATH, and MSVC /O2 through the newest Visual Studio's vcvars64 +$vs = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -prerelease -property installationPath +$vcvars = "$vs\VC\Auxiliary\Build\vcvars64.bat" +foreach ($f in "idiomatic", "shared") { + $src = "$PSScriptRoot\cpp\$f.cpp" + if (Get-Command clang++ -ErrorAction SilentlyContinue) { + $exe = "$out\cpp_${f}_clang.exe" + clang++ -std=c++20 -O3 -o $exe $src + if ($LASTEXITCODE -eq 0) { $bins += @{ n = "c++ $f (clang)"; p = $exe } } + } + if (Test-Path $vcvars) { + $exe = "$out\cpp_${f}_msvc.exe" + cmd /c "`"$vcvars`" >nul && cl /nologo /std:c++20 /O2 /EHsc /MT /Fe:`"$exe`" /Fo:`"$out\\`" `"$src`"" | Out-Null + if ($LASTEXITCODE -eq 0) { $bins += @{ n = "c++ $f (msvc)"; p = $exe } } + } +} + foreach ($b in $bins) { $text = (& $b.p) -join " " $ok = $text -match "38837369|low digits: 369" diff --git a/tslang/benchmarks/raytrace/cpp/idiomatic.cpp b/tslang/benchmarks/raytrace/cpp/idiomatic.cpp new file mode 100644 index 000000000..c29b1427d --- /dev/null +++ b/tslang/benchmarks/raytrace/cpp/idiomatic.cpp @@ -0,0 +1,230 @@ +// Idiomatic C++ port of rtbench.ts: Vector/Color are plain value structs, things are +// std::unique_ptr with virtual methods, surfaces hold function pointers. +#include +#include +#include +#include +#include +#include +#include + +struct Vector { + double x, y, z; + static Vector times(double k, Vector v) { return {k * v.x, k * v.y, k * v.z}; } + static Vector minus(Vector a, Vector b) { return {a.x - b.x, a.y - b.y, a.z - b.z}; } + static Vector plus(Vector a, Vector b) { return {a.x + b.x, a.y + b.y, a.z + b.z}; } + static double dot(Vector a, Vector b) { return a.x * b.x + a.y * b.y + a.z * b.z; } + static double mag(Vector v) { return std::sqrt(v.x * v.x + v.y * v.y + v.z * v.z); } + static Vector norm(Vector v) { + double m = mag(v); + double div = (m == 0) ? std::numeric_limits::infinity() : 1.0 / m; + return times(div, v); + } + static Vector cross(Vector a, Vector b) { + return {a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x}; + } +}; + +struct Color { + double r, g, b; + static Color scale(double k, Color v) { return {k * v.r, k * v.g, k * v.b}; } + static Color plus(Color a, Color b) { return {a.r + b.r, a.g + b.g, a.b + b.b}; } + static Color times(Color a, Color b) { return {a.r * b.r, a.g * b.g, a.b * b.b}; } +}; + +constexpr Color white{1.0, 1.0, 1.0}; +constexpr Color grey{0.5, 0.5, 0.5}; +constexpr Color black{0.0, 0.0, 0.0}; +constexpr Color background = black; +constexpr Color defaultColor = black; + +struct DrawingColor { double r, g, b; }; + +static DrawingColor toDrawingColor(Color c) { + auto legalize = [](double d) { return d > 1 ? 1.0 : d; }; + return {std::floor(legalize(c.r) * 255), std::floor(legalize(c.g) * 255), std::floor(legalize(c.b) * 255)}; +} + +struct Camera { + Vector pos, forward, right, up; + Camera(Vector pos, Vector lookAt) : pos(pos) { + Vector down{0.0, -1.0, 0.0}; + forward = Vector::norm(Vector::minus(lookAt, pos)); + right = Vector::times(1.5, Vector::norm(Vector::cross(forward, down))); + up = Vector::times(1.5, Vector::norm(Vector::cross(forward, right))); + } +}; + +struct Ray { Vector start, dir; }; + +struct Thing; +struct Intersection { const Thing *thing; Ray ray; double dist; }; + +struct Surface { + Color (*diffuse)(Vector); + Color (*specular)(Vector); + double (*reflect)(Vector); + double roughness; +}; + +struct Thing { + virtual ~Thing() = default; + virtual std::optional intersect(const Ray &ray) const = 0; + virtual Vector normal(Vector pos) const = 0; + const Surface *surface; + explicit Thing(const Surface *surface) : surface(surface) {} +}; + +struct Light { Vector pos; Color color; }; + +struct Scene { + std::vector> things; + std::vector lights; + Camera camera; +}; + +struct Sphere : Thing { + Vector center; + double radius2; + Sphere(Vector center, double radius, const Surface *surface) : Thing(surface), center(center), radius2(radius * radius) {} + Vector normal(Vector pos) const override { return Vector::norm(Vector::minus(pos, center)); } + std::optional intersect(const Ray &ray) const override { + Vector eo = Vector::minus(center, ray.start); + double v = Vector::dot(eo, ray.dir); + double dist = 0; + if (v >= 0) { + double disc = radius2 - (Vector::dot(eo, eo) - v * v); + if (disc >= 0) dist = v - std::sqrt(disc); + } + if (dist == 0) return std::nullopt; + return Intersection{this, ray, dist}; + } +}; + +struct Plane : Thing { + Vector norm; + double offset; + Plane(Vector norm, double offset, const Surface *surface) : Thing(surface), norm(norm), offset(offset) {} + Vector normal(Vector) const override { return norm; } + std::optional intersect(const Ray &ray) const override { + double denom = Vector::dot(norm, ray.dir); + if (denom > 0) return std::nullopt; + double dist = (Vector::dot(norm, ray.start) + offset) / (-denom); + return Intersection{this, ray, dist}; + } +}; + +static bool odd(Vector pos) { return std::fmod(std::floor(pos.z) + std::floor(pos.x), 2) != 0; } + +const Surface shiny{ + [](Vector) { return white; }, + [](Vector) { return grey; }, + [](Vector) { return 0.7; }, + 250.0}; + +const Surface checkerboard{ + [](Vector pos) { return odd(pos) ? white : black; }, + [](Vector) { return white; }, + [](Vector pos) { return odd(pos) ? 0.1 : 0.7; }, + 150.0}; + +struct RayTracer { + int maxDepth = 5; + + std::optional intersections(const Ray &ray, const Scene &scene) const { + double closest = std::numeric_limits::infinity(); + std::optional closestInter; + for (auto &thing : scene.things) { + auto inter = thing->intersect(ray); + if (inter && inter->dist < closest) { + closestInter = inter; + closest = inter->dist; + } + } + return closestInter; + } + + std::optional testRay(const Ray &ray, const Scene &scene) const { + auto isect = intersections(ray, scene); + if (isect) return isect->dist; + return std::nullopt; + } + + Color traceRay(const Ray &ray, const Scene &scene, int depth) const { + auto isect = intersections(ray, scene); + if (!isect) return background; + return shade(*isect, scene, depth); + } + + Color shade(const Intersection &isect, const Scene &scene, int depth) const { + Vector d = isect.ray.dir; + Vector pos = Vector::plus(Vector::times(isect.dist, d), isect.ray.start); + Vector normal = isect.thing->normal(pos); + Vector reflectDir = Vector::minus(d, Vector::times(2, Vector::times(Vector::dot(normal, d), normal))); + Color naturalColor = Color::plus(background, getNaturalColor(isect.thing, pos, normal, reflectDir, scene)); + Color reflectedColor = (depth >= maxDepth) ? grey : getReflectionColor(isect.thing, pos, normal, reflectDir, scene, depth); + return Color::plus(naturalColor, reflectedColor); + } + + Color getReflectionColor(const Thing *thing, Vector pos, Vector, Vector rd, const Scene &scene, int depth) const { + return Color::scale(thing->surface->reflect(pos), traceRay({pos, rd}, scene, depth + 1)); + } + + Color getNaturalColor(const Thing *thing, Vector pos, Vector norm, Vector rd, const Scene &scene) const { + auto addLight = [&](Color col, const Light &light) { + Vector ldis = Vector::minus(light.pos, pos); + Vector livec = Vector::norm(ldis); + auto neatIsect = testRay({pos, livec}, scene); + bool isInShadow = neatIsect ? (*neatIsect <= Vector::mag(ldis)) : false; + if (isInShadow) return col; + double illum = Vector::dot(livec, norm); + Color lcolor = (illum > 0) ? Color::scale(illum, light.color) : defaultColor; + double specular = Vector::dot(livec, Vector::norm(rd)); + Color scolor = (specular > 0) ? Color::scale(std::pow(specular, thing->surface->roughness), light.color) : defaultColor; + return Color::plus(col, Color::plus(Color::times(thing->surface->diffuse(pos), lcolor), + Color::times(thing->surface->specular(pos), scolor))); + }; + Color col = defaultColor; + for (auto &light : scene.lights) col = addLight(col, light); + return col; + } + + double render(const Scene &scene, double screenWidth, double screenHeight) const { + auto getPoint = [&](double x, double y, const Camera &camera) { + auto recenterX = [&](double x) { return (x - (screenWidth / 2.0)) / 2.0 / screenWidth; }; + auto recenterY = [&](double y) { return -(y - (screenHeight / 2.0)) / 2.0 / screenHeight; }; + return Vector::norm(Vector::plus(camera.forward, Vector::plus(Vector::times(recenterX(x), camera.right), Vector::times(recenterY(y), camera.up)))); + }; + double sum = 0; + for (double y = 0; y < screenHeight; y++) { + for (double x = 0; x < screenWidth; x++) { + Color color = traceRay({scene.camera.pos, getPoint(x, y, scene.camera)}, scene, 0); + DrawingColor c = toDrawingColor(color); + sum += c.r + c.g + c.b; + } + } + return sum; + } +}; + +static Scene defaultScene() { + Scene scene{{}, {}, Camera({3.0, 2.0, 4.0}, {-1.0, 0.5, 0.0})}; + scene.things.push_back(std::make_unique(Vector{0.0, 1.0, 0.0}, 0.0, &checkerboard)); + scene.things.push_back(std::make_unique(Vector{0.0, 1.0, -0.25}, 1.0, &shiny)); + scene.things.push_back(std::make_unique(Vector{-6, 3.0, -5.0}, 1.0, &shiny)); + scene.things.push_back(std::make_unique(Vector{-1.0, 0.5, 1.5}, 0.5, &shiny)); + scene.lights = {{{-2.0, 2.5, 0.0}, {0.49, 0.07, 0.07}}, + {{1.5, 2.5, 1.5}, {0.07, 0.07, 0.49}}, + {{1.5, 2.5, -1.5}, {0.07, 0.49, 0.071}}, + {{0.0, 3.5, 0.0}, {0.21, 0.21, 0.35}}}; + return scene; +} + +int main(int argc, char **argv) { + std::puts("start..."); + double size = argc > 1 ? std::atof(argv[1]) : 512; + RayTracer rayTracer; + double sum = rayTracer.render(defaultScene(), size, size); + std::printf("checksum: %.0f\n", sum); + std::puts("done."); +} diff --git a/tslang/benchmarks/raytrace/cpp/shared.cpp b/tslang/benchmarks/raytrace/cpp/shared.cpp new file mode 100644 index 000000000..aa91b25a2 --- /dev/null +++ b/tslang/benchmarks/raytrace/cpp/shared.cpp @@ -0,0 +1,245 @@ +// C++ port of rtbench.ts that keeps the TypeScript allocation pattern, like rust/src/bin/boxed.rs: +// every Vector, Color, Ray and Intersection is a fresh std::shared_ptr (make_shared, atomic +// counts), things are shared_ptr, surface members are std::function. Same algorithm as +// idiomatic.cpp. +#include +#include +#include +#include +#include +#include +#include +#include + +struct Vector; +using V = std::shared_ptr; +struct Vector { + double x, y, z; + Vector(double x, double y, double z) : x(x), y(y), z(z) {} + static V make(double x, double y, double z) { return std::make_shared(x, y, z); } + static V times(double k, const V &v) { return make(k * v->x, k * v->y, k * v->z); } + static V minus(const V &a, const V &b) { return make(a->x - b->x, a->y - b->y, a->z - b->z); } + static V plus(const V &a, const V &b) { return make(a->x + b->x, a->y + b->y, a->z + b->z); } + static double dot(const V &a, const V &b) { return a->x * b->x + a->y * b->y + a->z * b->z; } + static double mag(const V &v) { return std::sqrt(v->x * v->x + v->y * v->y + v->z * v->z); } + static V norm(const V &v) { + double m = mag(v); + double div = (m == 0) ? std::numeric_limits::infinity() : 1.0 / m; + return times(div, v); + } + static V cross(const V &a, const V &b) { + return make(a->y * b->z - a->z * b->y, a->z * b->x - a->x * b->z, a->x * b->y - a->y * b->x); + } +}; + +struct Color; +using C = std::shared_ptr; +struct Color { + double r, g, b; + Color(double r, double g, double b) : r(r), g(g), b(b) {} + static C make(double r, double g, double b) { return std::make_shared(r, g, b); } + static C scale(double k, const C &v) { return make(k * v->r, k * v->g, k * v->b); } + static C plus(const C &a, const C &b) { return make(a->r + b->r, a->g + b->g, a->b + b->b); } + static C times(const C &a, const C &b) { return make(a->r * b->r, a->g * b->g, a->b * b->b); } +}; + +static const C white = Color::make(1.0, 1.0, 1.0); +static const C grey = Color::make(0.5, 0.5, 0.5); +static const C black = Color::make(0.0, 0.0, 0.0); +static const C background = black; +static const C defaultColor = black; + +struct DrawingColor { double r, g, b; }; + +static DrawingColor toDrawingColor(const C &c) { + auto legalize = [](double d) { return d > 1 ? 1.0 : d; }; + return {std::floor(legalize(c->r) * 255), std::floor(legalize(c->g) * 255), std::floor(legalize(c->b) * 255)}; +} + +struct Camera { + V pos, forward, right, up; + Camera(V pos, const V &lookAt) : pos(std::move(pos)) { + V down = Vector::make(0.0, -1.0, 0.0); + forward = Vector::norm(Vector::minus(lookAt, this->pos)); + right = Vector::times(1.5, Vector::norm(Vector::cross(forward, down))); + up = Vector::times(1.5, Vector::norm(Vector::cross(forward, right))); + } +}; + +struct Ray { V start, dir; }; +using R = std::shared_ptr; + +struct Thing; +struct Intersection { std::shared_ptr thing; R ray; double dist; }; +using I = std::shared_ptr; + +struct Surface { + std::function diffuse; + std::function specular; + std::function reflect; + double roughness; +}; + +struct Thing : std::enable_shared_from_this { + std::shared_ptr surface; + explicit Thing(std::shared_ptr surface) : surface(std::move(surface)) {} + virtual ~Thing() = default; + virtual I intersect(const R &ray) const = 0; + virtual V normal(const V &pos) const = 0; +}; + +struct Light { V pos; C color; }; + +struct Scene { + std::vector> things; + std::vector> lights; + std::shared_ptr camera; +}; + +struct Sphere : Thing { + V center; + double radius2; + Sphere(V center, double radius, std::shared_ptr surface) + : Thing(std::move(surface)), center(std::move(center)), radius2(radius * radius) {} + V normal(const V &pos) const override { return Vector::norm(Vector::minus(pos, center)); } + I intersect(const R &ray) const override { + V eo = Vector::minus(center, ray->start); + double v = Vector::dot(eo, ray->dir); + double dist = 0; + if (v >= 0) { + double disc = radius2 - (Vector::dot(eo, eo) - v * v); + if (disc >= 0) dist = v - std::sqrt(disc); + } + if (dist == 0) return nullptr; + return std::make_shared(Intersection{shared_from_this(), ray, dist}); + } +}; + +struct Plane : Thing { + V norm; + double offset; + Plane(V norm, double offset, std::shared_ptr surface) + : Thing(std::move(surface)), norm(std::move(norm)), offset(offset) {} + V normal(const V &) const override { return norm; } + I intersect(const R &ray) const override { + double denom = Vector::dot(norm, ray->dir); + if (denom > 0) return nullptr; + double dist = (Vector::dot(norm, ray->start) + offset) / (-denom); + return std::make_shared(Intersection{shared_from_this(), ray, dist}); + } +}; + +static bool odd(const V &pos) { return std::fmod(std::floor(pos->z) + std::floor(pos->x), 2) != 0; } + +struct RayTracer { + int maxDepth = 5; + + I intersections(const R &ray, const Scene &scene) const { + double closest = std::numeric_limits::infinity(); + I closestInter; + for (auto &thing : scene.things) { + I inter = thing->intersect(ray); + if (inter && inter->dist < closest) { + closestInter = inter; + closest = inter->dist; + } + } + return closestInter; + } + + std::optional testRay(const R &ray, const Scene &scene) const { + I isect = intersections(ray, scene); + if (isect) return isect->dist; + return std::nullopt; + } + + C traceRay(const R &ray, const Scene &scene, int depth) const { + I isect = intersections(ray, scene); + if (!isect) return background; + return shade(isect, scene, depth); + } + + C shade(const I &isect, const Scene &scene, int depth) const { + V d = isect->ray->dir; + V pos = Vector::plus(Vector::times(isect->dist, d), isect->ray->start); + V normal = isect->thing->normal(pos); + V reflectDir = Vector::minus(d, Vector::times(2, Vector::times(Vector::dot(normal, d), normal))); + C naturalColor = Color::plus(background, getNaturalColor(*isect->thing, pos, normal, reflectDir, scene)); + C reflectedColor = (depth >= maxDepth) ? grey : getReflectionColor(*isect->thing, pos, normal, reflectDir, scene, depth); + return Color::plus(naturalColor, reflectedColor); + } + + C getReflectionColor(const Thing &thing, const V &pos, const V &, const V &rd, const Scene &scene, int depth) const { + return Color::scale(thing.surface->reflect(pos), traceRay(std::make_shared(Ray{pos, rd}), scene, depth + 1)); + } + + C getNaturalColor(const Thing &thing, const V &pos, const V &norm, const V &rd, const Scene &scene) const { + std::function addLight = [&](const C &col, const Light &light) -> C { + V ldis = Vector::minus(light.pos, pos); + V livec = Vector::norm(ldis); + auto neatIsect = testRay(std::make_shared(Ray{pos, livec}), scene); + bool isInShadow = neatIsect ? (*neatIsect <= Vector::mag(ldis)) : false; + if (isInShadow) return col; + double illum = Vector::dot(livec, norm); + C lcolor = (illum > 0) ? Color::scale(illum, light.color) : defaultColor; + double specular = Vector::dot(livec, Vector::norm(rd)); + C scolor = (specular > 0) ? Color::scale(std::pow(specular, thing.surface->roughness), light.color) : defaultColor; + return Color::plus(col, Color::plus(Color::times(thing.surface->diffuse(pos), lcolor), + Color::times(thing.surface->specular(pos), scolor))); + }; + C col = defaultColor; + for (auto &light : scene.lights) col = addLight(col, *light); + return col; + } + + double render(const Scene &scene, double screenWidth, double screenHeight) const { + auto getPoint = [&](double x, double y, const Camera &camera) { + auto recenterX = [&](double x) { return (x - (screenWidth / 2.0)) / 2.0 / screenWidth; }; + auto recenterY = [&](double y) { return -(y - (screenHeight / 2.0)) / 2.0 / screenHeight; }; + return Vector::norm(Vector::plus(camera.forward, Vector::plus(Vector::times(recenterX(x), camera.right), Vector::times(recenterY(y), camera.up)))); + }; + double sum = 0; + for (double y = 0; y < screenHeight; y++) { + for (double x = 0; x < screenWidth; x++) { + C color = traceRay(std::make_shared(Ray{scene.camera->pos, getPoint(x, y, *scene.camera)}), scene, 0); + DrawingColor c = toDrawingColor(color); + sum += c.r + c.g + c.b; + } + } + return sum; + } +}; + +static Scene defaultScene() { + auto shiny = std::make_shared(Surface{ + [](const V &) { return white; }, + [](const V &) { return grey; }, + [](const V &) { return 0.7; }, + 250.0}); + auto checkerboard = std::make_shared(Surface{ + [](const V &pos) { return odd(pos) ? white : black; }, + [](const V &) { return white; }, + [](const V &pos) { return odd(pos) ? 0.1 : 0.7; }, + 150.0}); + Scene scene; + scene.things = {std::make_shared(Vector::make(0.0, 1.0, 0.0), 0.0, checkerboard), + std::make_shared(Vector::make(0.0, 1.0, -0.25), 1.0, shiny), + std::make_shared(Vector::make(-6, 3.0, -5.0), 1.0, shiny), + std::make_shared(Vector::make(-1.0, 0.5, 1.5), 0.5, shiny)}; + auto light = [](V pos, C color) { return std::make_shared(Light{std::move(pos), std::move(color)}); }; + scene.lights = {light(Vector::make(-2.0, 2.5, 0.0), Color::make(0.49, 0.07, 0.07)), + light(Vector::make(1.5, 2.5, 1.5), Color::make(0.07, 0.07, 0.49)), + light(Vector::make(1.5, 2.5, -1.5), Color::make(0.07, 0.49, 0.071)), + light(Vector::make(0.0, 3.5, 0.0), Color::make(0.21, 0.21, 0.35))}; + scene.camera = std::make_shared(Vector::make(3.0, 2.0, 4.0), Vector::make(-1.0, 0.5, 0.0)); + return scene; +} + +int main(int argc, char **argv) { + std::puts("start..."); + double size = argc > 1 ? std::atof(argv[1]) : 512; + RayTracer rayTracer; + double sum = rayTracer.render(defaultScene(), size, size); + std::printf("checksum: %.0f\n", sum); + std::puts("done."); +}