feat: add render graph driven renderer architecture

Amp-Thread-ID: https://ampcode.com/threads/T-019f9d91-77c1-7206-a60f-ed6554ce92ab

Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
Amp
2026-07-27 02:53:44 +00:00
co-authored by heaust
parent 8a8369706b
commit d4e8634f67
290 changed files with 48804 additions and 1995 deletions
+92
View File
@@ -0,0 +1,92 @@
export const enum DirtyReason {
Scene = 1,
Camera = 2,
Selection = 4,
Preview = 8,
Viewport = 16,
Barrier = 32,
HostInteraction = 64,
}
export interface SchedulerMetrics {
requests: number;
coalesced: number;
frames: number;
maxInFlight: number;
staleAcks: number;
}
type InvalidationTarget = { readonly scheduler: Pick<RenderScheduler, "request"> };
/** Worker-internal bridge from atlas invalidations to repaint requests. */
export function requestViewInvalidations(
viewIds: readonly string[],
views: ReadonlyMap<string, InvalidationTarget>,
): void {
for (const viewId of viewIds) views.get(viewId)?.scheduler.request(0, DirtyReason.Viewport);
}
export class RenderScheduler {
private dirty = 0;
private inFlight: number | undefined;
private scheduled = false;
private running = false;
private latestRenderId = 0;
private nextFrameId = 1;
private poll = () => {};
readonly metrics: SchedulerMetrics = { requests: 0, coalesced: 0, frames: 0, maxInFlight: 0, staleAcks: 0 };
constructor(
private readonly draw: (frameId: number, renderId: number, reasons: number) => void,
private readonly enqueue: (callback: () => void) => void = (callback) => {
const raf = (globalThis as { requestAnimationFrame?: (cb: () => void) => void }).requestAnimationFrame;
raf ? raf(callback) : setTimeout(callback, 16);
},
) {}
start(poll: () => void = () => {}): void {
if (this.running) return;
this.running = true;
this.poll = poll;
this.schedule();
}
stop(): void {
this.running = false;
}
request(renderId = this.latestRenderId, reason: DirtyReason = DirtyReason.Scene): void {
this.metrics.requests++;
this.latestRenderId = Math.max(this.latestRenderId, renderId);
if (this.dirty || this.inFlight !== undefined) this.metrics.coalesced++;
this.dirty |= reason;
}
consumed(frameId: number): void {
if (frameId !== this.inFlight) {
this.metrics.staleAcks++;
return;
}
this.inFlight = undefined;
}
defer(frameId: number, reasons: number): void {
if (frameId !== this.inFlight) return;
this.inFlight = undefined;
this.dirty |= reasons;
}
private schedule(): void {
if (!this.running || this.scheduled) return;
this.scheduled = true;
this.enqueue(() => {
this.scheduled = false;
if (!this.running) return;
try {
this.poll();
if (this.dirty && this.inFlight === undefined) {
const reasons = this.dirty;
this.dirty = 0;
const frameId = this.nextFrameId++;
this.inFlight = frameId;
this.metrics.frames++;
this.metrics.maxInFlight = Math.max(this.metrics.maxInFlight, 1);
this.draw(frameId, this.latestRenderId, reasons);
}
} finally {
this.schedule();
}
});
}
}