fix: bind performance evidence to production resources

This commit is contained in:
2026-09-02 08:53:00 +03:00
parent 5b7e47c2a2
commit e3ac3a4db4
23 changed files with 642 additions and 60 deletions

View File

@@ -24,6 +24,31 @@ export interface SceneResourceSnapshot {
rafCount: 0 | 1;
}
export interface GpuTextureAllocation {
id: string;
width: number;
height: number;
bytesPerPixel: number;
layers: number;
mipmapped: boolean;
}
export interface GpuRenderTargetAllocation {
id: string;
width: number;
height: number;
bytesPerPixel: number;
attachments: number;
samples: number;
}
export interface SceneGpuBudgetSnapshot {
tier: 'low' | 'high';
textures: readonly GpuTextureAllocation[];
geometryBytes: number;
renderTargets: readonly GpuRenderTargetAllocation[];
}
const resourceKinds: readonly SceneResourceKind[] = [
'geometry', 'material', 'texture', 'target', 'pass', 'renderer', 'other',
];
@@ -127,4 +152,67 @@ export class SceneResourceBag {
rafCount: this.raf ? 1 : 0,
};
}
gpuBudgetSnapshot(input: {
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 }];
let textureIndex = 0;
let targetIndex = 0;
for (const [resource, kind] of this.resources) {
if (kind === 'geometry') {
const geometry = resource as DisposableResource & {
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),
geometry.index?.array,
].filter((array): array is ArrayBufferView => array !== undefined);
for (const array of arrays) {
if (seenBuffers.has(array.buffer)) continue;
seenBuffers.add(array.buffer);
geometryBytes += array.byteLength;
}
}
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.');
}
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 });
textureIndex += 1;
}
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.');
}
renderTargets.push({
id: `owned-render-target-${targetIndex}`,
width,
height,
bytesPerPixel: 4,
attachments: target.depthBuffer === false ? 1 : 2,
samples: Math.max(1, Number(target.samples) || 1),
});
targetIndex += 1;
}
}
return { tier: input.tier, textures, geometryBytes, renderTargets };
}
}

View File

@@ -136,6 +136,18 @@ export async function createProceduralSceneGeneration(
Math.max(1, Math.floor(width * quality.pixelRatio)),
Math.max(1, Math.floor(height * quality.pixelRatio)),
);
if (TEST_BUILD && context.quality !== 'medium') {
window.__PORTFOLIO_PERFORMANCE__?.recordRuntimeAllocation(context.resources.gpuBudgetSnapshot({
tier: context.quality,
defaultFramebuffer: {
width: Math.max(1, canvas.width),
height: Math.max(1, canvas.height),
bytesPerPixel: 4,
attachments: 2,
samples: context.quality === 'low' ? 1 : 4,
},
}));
}
};
if (context.signal.aborted) {