aboutsummaryrefslogtreecommitdiff
path: root/src/RaytraceDispatcher.ts
diff options
context:
space:
mode:
authorJames Barnett <noreply@jamesbarnett.xyz>2025-07-26 11:17:10 +0100
committerJames Barnett <noreply@jamesbarnett.xyz>2025-07-26 11:17:10 +0100
commit10bfc58085c6ab7e62077a1a9b6a6d922fffb025 (patch)
tree3e1d3ff39891c2c2408235c209566f4c51915859 /src/RaytraceDispatcher.ts
parentebc500c522e7233fbc6037bce0569edf3a0b5136 (diff)
downloadjs-raytracer-10bfc58085c6ab7e62077a1a9b6a6d922fffb025.tar.xz
js-raytracer-10bfc58085c6ab7e62077a1a9b6a6d922fffb025.zip
Rework multithreaded rendering
Render worker tasks are now split into chunks rather than row blocks for better thread utilisation
Diffstat (limited to 'src/RaytraceDispatcher.ts')
-rw-r--r--src/RaytraceDispatcher.ts154
1 files changed, 101 insertions, 53 deletions
diff --git a/src/RaytraceDispatcher.ts b/src/RaytraceDispatcher.ts
index be36bb1..6d1bbd7 100644
--- a/src/RaytraceDispatcher.ts
+++ b/src/RaytraceDispatcher.ts
@@ -1,13 +1,20 @@
import {Framebuffer} from './Framebuffer';
-import {RaytraceContext} from './RaytraceContext';
+import {RaytraceContext} from './models/RaytraceContext';
import {instanceToPlain} from 'class-transformer';
import 'reflect-metadata';
import {Logger} from './Logger';
+import {FrameChunk} from "./models/FrameChunk";
+import {Colour} from "./models/Colour";
export class RaytraceDispatcher {
- private renderStartMs: number;
- private completedThreads = 0;
- private processedResponses = 0;
+ private readonly renderStartMs: number;
+ private readonly contextJson: String;
+ private readonly chunks: FrameChunk[];
+ private readonly raytraceWorkers: Worker[];
+ private nextChunkIndex = 0;
+ private completedWorkers = 0;
+ private chunkBorderWidth = 1;
+
constructor(
readonly framebuffer: Framebuffer,
readonly context: RaytraceContext,
@@ -15,74 +22,115 @@ export class RaytraceDispatcher {
readonly onComplete: Function
) {
this.renderStartMs = new Date().getTime();
+ this.contextJson = JSON.stringify(instanceToPlain(context))
+ this.chunks = [];
+ this.raytraceWorkers = [];
}
requestRender() {
- // Assumes height%threads = 0
- const rowBatchSize = this.context.height / this.context.options.numThreads;
+ let chunkHeight, chunkWidth;
- for (let y = 0; y < this.context.height; y += rowBatchSize) {
- const rowStartIndex = y;
- const rowEndIndex = y + rowBatchSize - 1;
- this.dispatchRaytraceWorker(rowStartIndex, rowEndIndex, this.context);
+ if (this.context.height <= 720) {
+ chunkHeight = 64;
+ chunkWidth = 64;
+ this.chunkBorderWidth = 2;
+ } else {
+ chunkHeight = 128;
+ chunkWidth = 128;
+ this.chunkBorderWidth = 5;
}
- }
- private dispatchRaytraceWorker(
- rowStartIndex: number,
- rowEndIndex: number,
- context: RaytraceContext
- ) {
- this.logger.log(`Dispatching worker: rows ${rowStartIndex}-${rowEndIndex}`);
+ // Process scene into chunks
+ for (let y = 0; y < this.context.height; y+= chunkHeight) {
+ for (let x = 0 ; x < this.context.width; x+= chunkWidth) {
+ this.chunks.push(new FrameChunk(x, y, chunkWidth, chunkHeight));
+ }
+ }
+ this.logger.log(`Scene split into ${this.chunks.length} chunks of ${chunkWidth}x${chunkHeight}`);
- const raytracer = new Worker(new URL('./Raytracer.ts', import.meta.url));
+ // Spawn worker threads
+ for (let n = 0; n < this.context.options.numThreads; n++) {
+ const worker = new Worker(new URL('./Raytracer.ts', import.meta.url));
+ worker.onmessage = (event) => { this.onMessageHandler(worker, event) };
+ this.raytraceWorkers.push(worker);
+ }
+ this.logger.log(`Spawned ${this.context.options.numThreads} render threads`);
- raytracer.postMessage({
+ // Start raytrace
+ for (let worker of this.raytraceWorkers) {
+ this.raytraceNextChunk(worker);
+ }
+ }
+
+ private raytraceNextChunk(worker: Worker) {
+ const chunkIndex = this.nextChunkIndex++;
+ let chunk = this.chunks[chunkIndex];
+ this.drawChunkBorder(chunk, this.chunkBorderWidth);
+ worker.postMessage({
type: 'raytraceStart',
- rowStartIndex: rowStartIndex,
- rowEndIndex: rowEndIndex,
- context: JSON.stringify(instanceToPlain(context)),
+ chunk: chunk,
+ chunkIndex: chunkIndex,
+ context: this.contextJson,
});
-
- raytracer.onmessage = ({data}) => {
- if (data.type === 'raytraceResultRow') {
- this.processResponse(data.rowIndex, data.resultBuffer);
- }
- if (data.type === 'raytraceComplete') {
- this.handleWorkerComplete();
- }
- };
}
- private handleWorkerComplete() {
- if (++this.completedThreads === this.context.options.numThreads) {
- this.framebuffer.flush();
- this.logger.log(
- `Raytrace completed in ${new Date().getTime() - this.renderStartMs}ms`
- );
+ private onMessageHandler(worker: Worker, message: MessageEvent) {
+ if (message.data.type === 'raytraceComplete') {
+ this.writeChunkToFramebuffer(message.data.chunkIndex, message.data.resultBuffer);
+ }
+
+ // Queue next work if available
+ if (this.nextChunkIndex < this.chunks.length) {
+ this.raytraceNextChunk(worker);
+ } else {
+ worker.terminate();
+ this.completedWorkers++;
+ }
+
+ if (this.completedWorkers == this.context.options.numThreads) {
this.onComplete();
+ this.logger.log(`Raytrace completed in ${new Date().getTime() - this.renderStartMs}ms\n`);
}
}
- private processResponse(rowIndex: number, rowData: ArrayBuffer) {
- const clampedRowData = new Uint8ClampedArray(rowData);
+ private writeChunkToFramebuffer(chunkIndex: number, data: ArrayBuffer) {
+ const clampedRowData = new Uint8ClampedArray(data);
+ const chunk = this.chunks[chunkIndex];
+
+ for (let y = 0; y < chunk.height; y++) {
+ for (let x = 0; x < chunk.width; x++) {
+ const idx = (x * 3) + ((chunk.width * 3) * y);
+ const r = clampedRowData[idx];
+ const g = clampedRowData[idx + 1];
+ const b = clampedRowData[idx + 2];
+ const colour = new Colour(r, g, b);
- for (let x = 0; x < this.context.width; x++) {
- const idx = x * 3;
- const r = clampedRowData[idx];
- const g = clampedRowData[idx + 1];
- const b = clampedRowData[idx + 2];
- this.framebuffer.writePixelAt(x, rowIndex, r, g, b);
+ this.framebuffer.writePixelAt( (x+chunk.xStart), (y+chunk.yStart), colour);
+ }
}
- if (
- this.context.options.bufferDrawCalls &&
- ++this.processedResponses >= this.context.height * 0.05
- ) {
- this.framebuffer.flush();
- this.processedResponses = 0;
- } else if (!this.context.options.bufferDrawCalls) {
- this.framebuffer.flush();
+ this.framebuffer.flush();
+ }
+
+ private drawChunkBorder(chunk: FrameChunk, borderWidth: number) {
+ const width = chunk.width;
+ const height = chunk.height;
+
+ const borderColour = new Colour(220, 128, 128);
+ const unrenderedAreaColour = new Colour(160, 160, 160);
+
+ for (let y = 0; y < height; y++) {
+ for (let x = 0; x < width; x++) {
+
+ if (y < borderWidth || y >= (height-borderWidth) || x < borderWidth || x >= (width-borderWidth)) {
+ this.framebuffer.writePixelAt(chunk.xStart+x, chunk.yStart+y, borderColour);
+ } else {
+ this.framebuffer.writePixelAt(chunk.xStart+x, chunk.yStart+y, unrenderedAreaColour);
+ }
+
+ }
}
+
+ this.framebuffer.flush();
}
}