From 2f4dc843d79094cdc94272171f02e99f7c4333da Mon Sep 17 00:00:00 2001 From: root Date: Wed, 9 Sep 2026 15:21:51 +0200 Subject: [PATCH] Ask the driver what filter a texture actually got Chasing the flaky pixel-art check needs to tell two things apart that look identical in a screenshot: a scale mode the model never changed, and one that changed but never reached the sampler. Only the driver can answer the second, so `__gm.textureFilter(file)` returns the model's scaleMode next to `gl.getTexParameter` on the uploaded texture. It binds through `renderer.texture.bindSource` rather than a raw `bindTexture`, so measuring does not corrupt Pixi's idea of what is bound and become a source of flakiness itself. `TextureLibrary.peek` looks a picture up without starting a load or touching its scale mode, which `get` would do. Sixty measurements later the filter path is cleared: model and driver agreed every time, across repeated switches, after the full module run-up, and through a forced context loss. The instrument stays; the note in todo-notes/flaky-pixel-art-filter.md stays open. --- src/editor/debugHook.ts | 31 +++++++++++++++++++++++++++++++ src/viewport/TextureLibrary.ts | 5 +++++ 2 files changed, 36 insertions(+) diff --git a/src/editor/debugHook.ts b/src/editor/debugHook.ts index 6602a60b..8eb23bd9 100644 --- a/src/editor/debugHook.ts +++ b/src/editor/debugHook.ts @@ -2,6 +2,7 @@ import { type Application, Color, type ColorSource, type Container, Rectangle, T import { resolveObjects } from '../core/model/instance'; import { findObjectHolder } from '../core/model/Project'; import { Button, ui, Widget } from '../ui'; +import { textureLibrary } from '../viewport/TextureLibrary'; import type { CentreTab, Editor } from './Editor'; import type { EditorState } from './state'; @@ -200,6 +201,36 @@ export function installDebugHook(app: Application, state: EditorState, editor: E const hex = (value: number | undefined) => (value ?? 0).toString(16).padStart(2, '0'); return `#${hex(pixels[0])}${hex(pixels[1])}${hex(pixels[2])}`; }, + /** + * What the GPU was actually told about a picture's filter, next to what the model asked for. + * The two can disagree – that is the difference between a wrong value and a value that never + * reached the sampler – and only the driver can be asked for the second one. + */ + textureFilter: (file: string) => { + const texture = textureLibrary.peek(file); + if (!texture) return null; + const source = texture.source; + const renderer = app.renderer as unknown as { + gl?: WebGL2RenderingContext; + uid: number; + texture: { bindSource(source: unknown, location: number): void }; + }; + const gl = renderer.gl; + const uploaded = Boolean( + (source as unknown as { _gpuData: Record })._gpuData[renderer.uid], + ); + let magFilter = 0; + if (gl) { + // Through Pixi rather than a raw `bindTexture`, so its idea of what is bound stays true. + renderer.texture.bindSource(source, 0); + magFilter = gl.getTexParameter(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER) as number; + } + return { + scaleMode: source.scaleMode, + gpu: magFilter === gl?.NEAREST ? 'nearest' : magFilter === gl?.LINEAR ? 'linear' : 'none', + uploaded, + }; + }, fps: () => editor.fps, tab: () => editor.tab, showTab: (tab: CentreTab) => editor.showTab(tab), diff --git a/src/viewport/TextureLibrary.ts b/src/viewport/TextureLibrary.ts index 129fe5a0..8f917da2 100644 --- a/src/viewport/TextureLibrary.ts +++ b/src/viewport/TextureLibrary.ts @@ -42,6 +42,11 @@ export class TextureLibrary { return texture; } + /** The picture if it is in memory, without starting a load or touching the scale mode. */ + peek(file: string): Texture | null { + return this.loaded.get(file) ?? null; + } + /** Runs `fn` whenever a picture has arrived; returns the unsubscribe. */ onChange(fn: () => void): () => void { this.listeners.add(fn);