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.
This commit is contained in:
root committed 2026-09-09 15:21:51 +02:00
1 parent e9634f0f0d
commit 2f4dc843d7
2 files changed
+36

No files matched your search

+31
View File
@@ -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<number, unknown> })._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),
+5
View File
@@ -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);