import { BindGroup, Texture, TextureSource } from 'pixi.js'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { TextureLibrary } from '../../src/viewport/TextureLibrary'; /** * #293: Pixi warned "[BindGroup] a 'textureSource' was destroyed while still bound to a shader" * with every change to a drawing that an object wore through a shader asset. The mesh's shader * names the picture until its next sync, and the library destroyed the old revision the moment it * drew the new one – which is before any wearer can have heard of it. * * Measured with Pixi's own bind group, which is what a shader keeps its resources in. */ function rig() { const library = new TextureLibrary(); const drawn: Texture[] = []; library.setPictureFactory(() => { const texture = new Texture({ source: new TextureSource({ width: 4, height: 4 }) }); drawn.push(texture); return texture; }); const warned = vi.spyOn(console, 'warn').mockImplementation(() => {}); const warnings = () => warned.mock.calls.map((call) => call.join(' ')).filter((line) => line.includes('BindGroup')); /** What a shader does with a picture it wears. */ const bind = (group: BindGroup, texture: Texture): void => { group.setResource(texture.source, 0); group.setResource(texture.source.style, 1); }; return { library, drawn, warnings, bind }; } afterEach(() => { vi.restoreAllMocks(); }); describe('a drawn picture a shader still names (#293)', () => { it('is told by the bind group listening on its source', () => { const r = rig(); const picture = r.library.load('gen:a@1')!; expect(picture.source.listenerCount('change')).toBe(0); const group = new BindGroup({ 0: picture.source, 1: picture.source.style }); expect(picture.source.listenerCount('change')).toBe(1); r.bind(group, Texture.WHITE); expect(picture.source.listenerCount('change')).toBe(0); }); it('is not destroyed under the shader when its drawing changes, and nothing is warned', () => { const r = rig(); const first = r.library.load('gen:a@1')!; const shader = new BindGroup({ 0: first.source, 1: first.source.style }); const second = r.library.load('gen:a@2')!; expect(second).not.toBe(first); expect(first.destroyed).toBe(false); expect(first.source.destroyed).toBe(false); expect(r.library.waitingPictures()).toEqual(['gen:a@1']); // It is no longer what the library hands out. expect(r.library.peek('gen:a@1')).toBeNull(); expect(r.warnings()).toEqual([]); // The mesh syncs: its shader takes the new picture. The next ask frees the old one. r.bind(shader, second); expect(r.library.load('gen:a@2')).toBe(second); expect(first.destroyed).toBe(true); expect(r.library.waitingPictures()).toEqual([]); expect(second.destroyed).toBe(false); expect(r.warnings()).toEqual([]); }); it('is freed right after the redraw in which the shader took the new picture', async () => { const r = rig(); const first = r.library.load('gen:a@1')!; const shader = new BindGroup({ 0: first.source, 1: first.source.style }); // One redraw: the mesh asks for its picture and hands it to its shader. r.bind(shader, r.library.load('gen:a@2')!); expect(first.destroyed).toBe(false); await Promise.resolve(); expect(first.destroyed).toBe(true); expect(r.library.waitingPictures()).toEqual([]); expect(r.warnings()).toEqual([]); }); it('stays through any number of changes while the shader does not sync, and only that one', () => { const r = rig(); const first = r.library.load('gen:a@1')!; const shader = new BindGroup({ 0: first.source, 1: first.source.style }); // The scene is not on screen: nobody syncs, the drawing is edited three times. r.library.load('gen:a@2'); r.library.load('gen:a@3'); r.library.load('gen:a@4'); expect(r.drawn.map((texture) => texture.destroyed)).toEqual([false, true, true, false]); expect(r.library.waitingPictures()).toEqual(['gen:a@1']); expect(r.warnings()).toEqual([]); shader.destroy(); r.library.load('gen:a@4'); expect(first.destroyed).toBe(true); expect(r.warnings()).toEqual([]); }); it('is destroyed right away when no shader names it', () => { const r = rig(); const first = r.library.load('gen:a@1')!; r.library.load('gen:a@2'); expect(first.destroyed).toBe(true); expect(r.library.waitingPictures()).toEqual([]); expect(r.warnings()).toEqual([]); }); });