fix: close final portfolio quality gaps

This commit is contained in:
2026-09-02 17:38:11 +03:00
parent 0899b6b00f
commit b4728ceef0
24 changed files with 1822 additions and 889 deletions

View File

@@ -1,11 +1,11 @@
export type SceneResourceKind =
| 'geometry'
| 'material'
| 'texture'
| 'target'
| 'pass'
| 'renderer'
| 'other';
| "geometry"
| "material"
| "texture"
| "target"
| "pass"
| "renderer"
| "other";
export interface DisposableResource {
dispose(): void;
@@ -42,15 +42,25 @@ export interface GpuRenderTargetAllocation {
samples: number;
}
export interface GpuRenderTargetReporter {
gpuRenderTargets(): readonly GpuRenderTargetAllocation[];
}
export interface SceneGpuBudgetSnapshot {
tier: 'low' | 'high';
tier: "low" | "high";
textures: readonly GpuTextureAllocation[];
geometryBytes: number;
renderTargets: readonly GpuRenderTargetAllocation[];
}
const resourceKinds: readonly SceneResourceKind[] = [
'geometry', 'material', 'texture', 'target', 'pass', 'renderer', 'other',
"geometry",
"material",
"texture",
"target",
"pass",
"renderer",
"other",
];
export class SceneResourceBag {
@@ -141,7 +151,9 @@ export class SceneResourceBag {
}
snapshot(): SceneResourceSnapshot {
const owned = Object.fromEntries(resourceKinds.map((kind) => [kind, 0])) as Record<SceneResourceKind, number>;
const owned = Object.fromEntries(
resourceKinds.map((kind) => [kind, 0]),
) as Record<SceneResourceKind, number>;
for (const kind of this.resources.values()) owned[kind] += 1;
return {
disposed: this.disposed,
@@ -154,23 +166,38 @@ export class SceneResourceBag {
}
gpuBudgetSnapshot(input: {
tier: 'low' | 'high';
defaultFramebuffer: Omit<GpuRenderTargetAllocation, 'id'>;
tier: "low" | "high";
defaultFramebuffer: Omit<GpuRenderTargetAllocation, "id">;
}): SceneGpuBudgetSnapshot {
let geometryBytes = 0;
const seenBuffers = new Set<ArrayBufferLike>();
const textures: GpuTextureAllocation[] = [];
const renderTargets: GpuRenderTargetAllocation[] = [{ id: 'default-framebuffer', ...input.defaultFramebuffer }];
const renderTargets: GpuRenderTargetAllocation[] = [
{ id: "default-framebuffer", ...input.defaultFramebuffer },
];
let textureIndex = 0;
let targetIndex = 0;
for (const [resource, kind] of this.resources) {
if (kind === 'geometry') {
const reporter = resource as DisposableResource &
Partial<GpuRenderTargetReporter>;
if (typeof reporter.gpuRenderTargets === "function") {
for (const allocation of reporter.gpuRenderTargets()) {
validateRenderTargetAllocation(allocation);
renderTargets.push({ ...allocation });
}
}
if (kind === "geometry") {
const geometry = resource as DisposableResource & {
attributes?: Record<string, { array?: ArrayBufferView; data?: { array?: ArrayBufferView } }>;
attributes?: Record<
string,
{ array?: ArrayBufferView; data?: { array?: ArrayBufferView } }
>;
index?: { array?: ArrayBufferView } | null;
};
const arrays = [
...Object.values(geometry.attributes ?? {}).map((attribute) => attribute.array ?? attribute.data?.array),
...Object.values(geometry.attributes ?? {}).map(
(attribute) => attribute.array ?? attribute.data?.array,
),
geometry.index?.array,
].filter((array): array is ArrayBufferView => array !== undefined);
for (const array of arrays) {
@@ -179,28 +206,58 @@ export class SceneResourceBag {
geometryBytes += array.byteLength;
}
}
if (kind === 'texture') {
if (kind === "texture") {
const texture = resource as DisposableResource & {
image?: { width?: number; height?: number; data?: ArrayBufferView };
generateMipmaps?: boolean;
};
const width = Number(texture.image?.width);
const height = Number(texture.image?.height);
if (!Number.isInteger(width) || width <= 0 || !Number.isInteger(height) || height <= 0) {
throw new Error('Owned production texture is missing measurable dimensions.');
if (
!Number.isInteger(width) ||
width <= 0 ||
!Number.isInteger(height) ||
height <= 0
) {
throw new Error(
"Owned production texture is missing measurable dimensions.",
);
}
const dataBytes = texture.image?.data?.byteLength;
const bytesPerPixel = dataBytes === undefined ? 4 : dataBytes / (width * height);
if (!Number.isInteger(bytesPerPixel) || bytesPerPixel <= 0) throw new Error('Owned production texture has an unsupported pixel layout.');
textures.push({ id: `owned-texture-${textureIndex}`, width, height, bytesPerPixel, layers: 1, mipmapped: texture.generateMipmaps !== false });
const bytesPerPixel =
dataBytes === undefined ? 4 : dataBytes / (width * height);
if (!Number.isInteger(bytesPerPixel) || bytesPerPixel <= 0)
throw new Error(
"Owned production texture has an unsupported pixel layout.",
);
textures.push({
id: `owned-texture-${textureIndex}`,
width,
height,
bytesPerPixel,
layers: 1,
mipmapped: texture.generateMipmaps !== false,
});
textureIndex += 1;
}
if (kind === 'target') {
const target = resource as DisposableResource & { width?: number; height?: number; samples?: number; depthBuffer?: boolean };
if (kind === "target") {
const target = resource as DisposableResource & {
width?: number;
height?: number;
samples?: number;
depthBuffer?: boolean;
};
const width = Number(target.width);
const height = Number(target.height);
if (!Number.isInteger(width) || width <= 0 || !Number.isInteger(height) || height <= 0) {
throw new Error('Owned production render target is missing measurable dimensions.');
if (
!Number.isInteger(width) ||
width <= 0 ||
!Number.isInteger(height) ||
height <= 0
) {
throw new Error(
"Owned production render target is missing measurable dimensions.",
);
}
renderTargets.push({
id: `owned-render-target-${targetIndex}`,
@@ -216,3 +273,25 @@ export class SceneResourceBag {
return { tier: input.tier, textures, geometryBytes, renderTargets };
}
}
function validateRenderTargetAllocation(
allocation: GpuRenderTargetAllocation,
): void {
if (
allocation.id.length === 0 ||
!Number.isInteger(allocation.width) ||
allocation.width <= 0 ||
!Number.isInteger(allocation.height) ||
allocation.height <= 0 ||
!Number.isInteger(allocation.bytesPerPixel) ||
allocation.bytesPerPixel <= 0 ||
!Number.isInteger(allocation.attachments) ||
allocation.attachments <= 0 ||
!Number.isInteger(allocation.samples) ||
allocation.samples <= 0
) {
throw new Error(
"Owned post-processing render target has an invalid allocation description.",
);
}
}