Files
Sanctification/card-harness/scripts/render-diagnostics.test.mjs

131 lines
5.0 KiB
JavaScript

import { test } from 'node:test'
import assert from 'node:assert/strict'
import * as THREE from 'three'
import './test-support.mjs'
const { drawingPixelRatio, FramePacing, GpuTimer } = await import('../src/renderDiagnostics.ts')
const { createCardMaterial, applyEdgeMaterialControls } = await import('../src/cardMaterial.ts')
const { createCardGeometry } = await import('../src/cardGeometry.ts')
test('full-resolution rendering retains native pixel ratio up to the existing 2x cap', () => {
for (const dpr of [1, 1.25, 2, 3]) {
assert.equal(drawingPixelRatio(dpr), Math.min(dpr, 2))
}
})
test('callback telemetry distinguishes idle pacing from intervals after drawing', () => {
const pacing = new FramePacing()
pacing.observe(0)
pacing.didDraw(false)
pacing.observe(16)
pacing.didDraw(true)
pacing.observe(49)
assert.deepEqual(pacing.idle, [16])
assert.deepEqual(pacing.drawing, [33])
pacing.reset()
pacing.observe(5000)
assert.deepEqual(pacing.idle, [])
assert.deepEqual(pacing.drawing, [])
})
function fakeGL(supported = true) {
const ext = { TIME_ELAPSED_EXT: 1, GPU_DISJOINT_EXT: 2, QUERY_COUNTER_BITS_EXT: 3 }
const reads = []
const deleted = []
const queries = []
let disjoint = false
const gl = {
QUERY_RESULT_AVAILABLE: 4, QUERY_RESULT: 5,
getExtension() { return supported ? ext : null },
getQuery() { return 64 },
createQuery() { const q = { available: false, ns: 4000000 }; queries.push(q); return q },
beginQuery() {}, endQuery() {}, isContextLost() { return false },
getParameter() { return disjoint },
getQueryParameter(query, parameter) {
reads.push(parameter)
return parameter === this.QUERY_RESULT_AVAILABLE ? query.available : query.ns
},
deleteQuery(query) { deleted.push(query) },
}
return { gl, queries, reads, deleted, disjoint() { disjoint = true } }
}
test('GPU timing reads results only after availability, with bounded sampling', () => {
const { gl, queries, reads, deleted } = fakeGL()
const timer = new GpuTimer(gl)
const delivered = []
timer.onSample = (timestampMs, elapsedMs) => delivered.push([timestampMs, elapsedMs])
timer.begin(0)
timer.end()
timer.poll()
assert.deepEqual(reads, [gl.QUERY_RESULT_AVAILABLE])
assert.deepEqual(timer.samples, [])
timer.begin(100)
timer.end()
assert.equal(queries.length, 1)
queries[0].available = true
timer.poll()
assert.deepEqual(timer.samples, [4])
assert.deepEqual(delivered, [[0, 4]])
assert.deepEqual(deleted, [queries[0]])
})
test('GPU disjoint results are discarded and outstanding query count is bounded', () => {
const { gl, queries, reads, deleted, disjoint } = fakeGL()
const timer = new GpuTimer(gl)
for (let i = 0; i < 20; i++) { timer.begin(i * 200); timer.end() }
assert.equal(queries.length, 8)
queries.forEach(q => { q.available = true })
disjoint()
timer.poll()
assert.deepEqual(reads, [])
assert.deepEqual(timer.samples, [])
assert.equal(deleted.length, 8)
})
test('unsupported GPU timing adds no queries and reset releases pending work', () => {
const unsupported = fakeGL(false)
const timer = new GpuTimer(unsupported.gl)
timer.begin(0); timer.end(); timer.poll()
assert.equal(timer.supported, false)
assert.equal(unsupported.queries.length, 0)
const supported = fakeGL()
const active = new GpuTimer(supported.gl)
active.begin(0)
active.reset()
assert.equal(supported.deleted.length, 1)
active.poll()
assert.deepEqual(supported.reads, [])
})
test('uniform-only edge changes reuse the shader; clearcoat feature changes invalidate it', () => {
const edge = new THREE.MeshPhysicalMaterial()
applyEdgeMaterialControls(edge, { substrate: 'Paper', condition: 1 })
const version = edge.version
applyEdgeMaterialControls(edge, { substrate: 'Metal', condition: 0.5 })
assert.equal(edge.version, version)
applyEdgeMaterialControls(edge, { substrate: 'Linen', condition: 1 })
assert.equal(edge.version, version + 1)
edge.dispose()
})
test('card artwork faces point outward on both sides with backface culling', () => {
const textures = Array.from({ length: 3 }, () => new THREE.Texture())
const front = createCardMaterial(...textures, new THREE.CubeTexture(), new THREE.Vector3())
const back = new THREE.MeshPhysicalMaterial() // Default FrontSide, as in the app.
const edge = new THREE.MeshPhysicalMaterial()
const card = createCardGeometry(front, back, edge)
card.updateMatrixWorld(true)
const frontFace = card.getObjectByName('CARD_FRONT')
const backFace = card.getObjectByName('CARD_BACK')
assert.equal(front.side, THREE.FrontSide)
assert.equal(back.side, THREE.FrontSide)
const normal = new THREE.Vector3(0, 0, 1)
assert.ok(normal.clone().transformDirection(frontFace.matrixWorld).z > 0.999)
assert.ok(normal.clone().transformDirection(backFace.matrixWorld).z < -0.999)
const geometries = new Set([frontFace.geometry, backFace.geometry, card.getObjectByName('CARD_EDGE').geometry])
geometries.forEach(g => g.dispose())
textures.forEach(t => t.dispose())
front.uniforms.studioEnvironment.value.dispose()
front.dispose(); back.dispose(); edge.dispose()
})