openvibers/openvibe.games Commits
Fix editor E2E placement and disposed texture fallback (#324)
Changes
@@@ -240,9 +240,12 @@ const browser = await chromium.launch({args: ['--use-angle=swiftshader', '--enable-webgl', '--disable-gpu-sandbox'],})const page = await browser.newPage({ viewport: { width: 1400, height: 900 } })--page.on('pageerror', (e) =>-- console.log('[pageerror]', String((e as Error).stack ?? e).slice(0, 500)),--)++let disposedTextureErrors = 0++const onPageError = (e: Error): void => {++ if (e.message.includes('markAllMaterialsAsDirty')) disposedTextureErrors++++ console.log('[pageerror]', String(e.stack ?? e).slice(0, 500))++}++page.on('pageerror', onPageError)await page.goto(`http://127.0.0.1:${PORT}/editor`)await page.waitForSelector('#save', { timeout: 90_000 })await page.waitForFunction(() => Boolean((window as never as { __editor?: unknown }).__editor), {@@@ -794,9 +797,7 @@ async function withMap(localStorage.setItem('ovg_sso', t as string)document.cookie = `ovg_sso=${encodeURIComponent(t as string)}; Path=/; SameSite=Lax`}, TEST_TOKEN)-- pg.on('pageerror', (e) =>-- console.log('[pageerror]', String((e as Error).stack ?? e).slice(0, 500)),-- )++ pg.on('pageerror', onPageError)await pg.goto(`http://127.0.0.1:${port}/editor`)await pg.waitForSelector('#save', { timeout: 90_000 })await pg.waitForFunction(@@@ -1686,14 +1687,40 @@ section('W. Painting imported models')zones: [],},async (pg) => {-- /** Arm a model and click the viewport to place one, returning its id. */++ /** Arm a model and click visible terrain, clear of the placement strip. */const place = async (modelId: string, x: number, y: number): Promise<string | null> => {await probeArgs(pg, ([p, id]) => p.armModel(id as unknown as string), modelId)-- await pg.mouse.move(x, y)++ const target = (await probeArgs(++ pg,++ ([p, px, py]) => {++ const canvas = document.getElementById('game')++ for (const [dx, dy] of [++ [0, 0],++ [0, -60],++ [-80, -60],++ [80, -60],++ [0, -120],++ ]) {++ const cx = (px as number) + dx++ const cy = (py as number) + dy++ if (document.elementFromPoint(cx, cy) === canvas && p.pickIdAt(cx, cy) === 'w-floor')++ return [cx, cy]++ }++ return null++ },++ x,++ y,++ )) as [number, number] | null++ if (!target) throw new Error(`no visible terrain near placement point (${x}, ${y})`)++ const before = (await probeOf(pg, (p) => p.objectCounts())).statics++ await pg.mouse.move(target[0], target[1])await pg.waitForTimeout(200)await pg.mouse.down()await pg.mouse.up()await pg.waitForTimeout(700)++ const after = (await probeOf(pg, (p) => p.objectCounts())).statics++ if (after !== before + 1)++ throw new Error(`model placement created ${after - before} objects`)return probeOf(pg, (p) => p.primaryId())}@@@ -1952,6 +1979,11 @@ section('V. Performance profile on a large map'))}++ok(++ 'disposed textures never retry KTX2 fallback',++ disposedTextureErrors === 0,++ disposedTextureErrors,++)console.log(`\n${'═'.repeat(64)}\n${checks - failures}/${checks} checks passed, ${failures} FAILED\n`,)@@@ -73,7 +73,9 @@ export function loadTexture(src: TexSource, scene: Scene): Texture {configure()let tx: Texture | null = nullconst fallBack = (): void => {-- if (tx && src.fallback) tx.updateURL(src.fallback)++ // KTX2 decoding can fail after a material has replaced and disposed this++ // texture. Babylon clears its scene on dispose; updateURL needs that scene.++ if (tx?.getScene() === scene && !scene.isDisposed && src.fallback) tx.updateURL(src.fallback)}tx = new Texture(src.url, scene, false, true, Texture.TRILINEAR_SAMPLINGMODE, null, fallBack)return tx
@@ -240,9 +240,12 @@ const browser = await chromium.launch({
@@ -240,9 +240,12 @@ const browser = await chromium.launch({
args: ['--use-angle=swiftshader', '--enable-webgl', '--disable-gpu-sandbox'],
args: ['--use-angle=swiftshader', '--enable-webgl', '--disable-gpu-sandbox'],
})
})
const page = await browser.newPage({ viewport: { width: 1400, height: 900 } })
const page = await browser.newPage({ viewport: { width: 1400, height: 900 } })
-page.on('pageerror', (e) =>
- console.log('[pageerror]', String((e as Error).stack ?? e).slice(0, 500)),
-)
+let disposedTextureErrors = 0
+const onPageError = (e: Error): void => {
+ if (e.message.includes('markAllMaterialsAsDirty')) disposedTextureErrors++
+ console.log('[pageerror]', String(e.stack ?? e).slice(0, 500))
+}
+page.on('pageerror', onPageError)
await page.goto(`http://127.0.0.1:${PORT}/editor`)
await page.goto(`http://127.0.0.1:${PORT}/editor`)
await page.waitForSelector('#save', { timeout: 90_000 })
await page.waitForSelector('#save', { timeout: 90_000 })
await page.waitForFunction(() => Boolean((window as never as { __editor?: unknown }).__editor), {
await page.waitForFunction(() => Boolean((window as never as { __editor?: unknown }).__editor), {
@@ -794,9 +797,7 @@ async function withMap(
@@ -794,9 +797,7 @@ async function withMap(
localStorage.setItem('ovg_sso', t as string)
localStorage.setItem('ovg_sso', t as string)
document.cookie = `ovg_sso=${encodeURIComponent(t as string)}; Path=/; SameSite=Lax`
document.cookie = `ovg_sso=${encodeURIComponent(t as string)}; Path=/; SameSite=Lax`
}, TEST_TOKEN)
}, TEST_TOKEN)
- pg.on('pageerror', (e) =>
- console.log('[pageerror]', String((e as Error).stack ?? e).slice(0, 500)),
- )
+ pg.on('pageerror', onPageError)
await pg.goto(`http://127.0.0.1:${port}/editor`)
await pg.goto(`http://127.0.0.1:${port}/editor`)
await pg.waitForSelector('#save', { timeout: 90_000 })
await pg.waitForSelector('#save', { timeout: 90_000 })
await pg.waitForFunction(
await pg.waitForFunction(
@@ -1686,14 +1687,40 @@ section('W. Painting imported models')
@@ -1686,14 +1687,40 @@ section('W. Painting imported models')
zones: [],
zones: [],
},
},
async (pg) => {
async (pg) => {
- /** Arm a model and click the viewport to place one, returning its id. */
+ /** Arm a model and click visible terrain, clear of the placement strip. */
const place = async (modelId: string, x: number, y: number): Promise<string | null> => {
const place = async (modelId: string, x: number, y: number): Promise<string | null> => {
await probeArgs(pg, ([p, id]) => p.armModel(id as unknown as string), modelId)
await probeArgs(pg, ([p, id]) => p.armModel(id as unknown as string), modelId)
- await pg.mouse.move(x, y)
+ const target = (await probeArgs(
+ pg,
+ ([p, px, py]) => {
+ const canvas = document.getElementById('game')
+ for (const [dx, dy] of [
+ [0, 0],
+ [0, -60],
+ [-80, -60],
+ [80, -60],
+ [0, -120],
+ ]) {
+ const cx = (px as number) + dx
+ const cy = (py as number) + dy
+ if (document.elementFromPoint(cx, cy) === canvas && p.pickIdAt(cx, cy) === 'w-floor')
+ return [cx, cy]
+ }
+ return null
+ },
+ x,
+ y,
+ )) as [number, number] | null
+ if (!target) throw new Error(`no visible terrain near placement point (${x}, ${y})`)
+ const before = (await probeOf(pg, (p) => p.objectCounts())).statics
+ await pg.mouse.move(target[0], target[1])
await pg.waitForTimeout(200)
await pg.waitForTimeout(200)
await pg.mouse.down()
await pg.mouse.down()
await pg.mouse.up()
await pg.mouse.up()
await pg.waitForTimeout(700)
await pg.waitForTimeout(700)
+ const after = (await probeOf(pg, (p) => p.objectCounts())).statics
+ if (after !== before + 1)
+ throw new Error(`model placement created ${after - before} objects`)
return probeOf(pg, (p) => p.primaryId())
return probeOf(pg, (p) => p.primaryId())
}
}
@@ -1952,6 +1979,11 @@ section('V. Performance profile on a large map')
@@ -1952,6 +1979,11 @@ section('V. Performance profile on a large map')
)
)
}
}
+ok(
+ 'disposed textures never retry KTX2 fallback',
+ disposedTextureErrors === 0,
+ disposedTextureErrors,
+)
console.log(
console.log(
`\n${'═'.repeat(64)}\n${checks - failures}/${checks} checks passed, ${failures} FAILED\n`,
`\n${'═'.repeat(64)}\n${checks - failures}/${checks} checks passed, ${failures} FAILED\n`,
)
)
@@ -73,7 +73,9 @@ export function loadTexture(src: TexSource, scene: Scene): Texture {
@@ -73,7 +73,9 @@ export function loadTexture(src: TexSource, scene: Scene): Texture {
configure()
configure()
let tx: Texture | null = null
let tx: Texture | null = null
const fallBack = (): void => {
const fallBack = (): void => {
- if (tx && src.fallback) tx.updateURL(src.fallback)
+ // KTX2 decoding can fail after a material has replaced and disposed this
+ // texture. Babylon clears its scene on dispose; updateURL needs that scene.
+ if (tx?.getScene() === scene && !scene.isDisposed && src.fallback) tx.updateURL(src.fallback)
}
}
tx = new Texture(src.url, scene, false, true, Texture.TRILINEAR_SAMPLINGMODE, null, fallBack)
tx = new Texture(src.url, scene, false, true, Texture.TRILINEAR_SAMPLINGMODE, null, fallBack)
return tx
return tx