Shaders draw in the sprite's own pass: a Mesh with the graph's fragment, no filter (#77)
game-maker / suite (push) Successful in 8m24s

A sprite's picture is now a Mesh (unit quad, sibling of its Graphics node, which keeps
only the hit area); a worn shader replaces Pixi's fragment in that draw, so there is no
intermediate texture and no extra render pass. Measured on the HD 630: 400 shaded
sprites hold 60 fps (was 34), 800 hold 56 (was 20).

Two Pixi facts the code leans on: a mesh's texture is bound only for Pixi's own shader,
so GraphShader.wear hands it over; and MeshPipe caches whether a mesh batches and
re-checks it only in a frame without structure changes, so switching a shader on or
off calls mesh.unload().

The console panel mirrors console.group/groupCollapsed/groupEnd and fills the %s %d
%o %c directives, so Pixi's deprecation warning shows its head, not only its stack.
E2E 12 guards against the addChild-on-Graphics warning, 28 counts shaders instead of
filters.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Fable 5.1 committed 2026-09-13 15:31:08 +02:00
1 parent 82effb2c03
commit 0e5e5ef925
15 files changed
+328 -88

No files matched your search

+48 -8
View File
@@ -36,30 +36,63 @@ function formatValue(value: unknown): string {
return String(value);
}
/** One line out of the arguments of a `console.*` call, the way the browser joins them. */
const HAS_DIRECTIVE = /%[sdifoOc%]/;
const DIRECTIVE = /%([sdifoOc%])/g;
/**
* One line out of the arguments of a `console.*` call, the way the browser joins them: a leading
* format string consumes one argument per directive, `%c` swallows its style, the rest follow.
*/
export function formatArgs(args: readonly unknown[]): string {
return args.map(formatValue).join(' ');
const [first, ...rest] = args;
if (typeof first !== 'string' || !HAS_DIRECTIVE.test(first)) return args.map(formatValue).join(' ');
const text = first.replace(DIRECTIVE, (match, spec: string) => {
if (spec === '%') return '%';
if (rest.length === 0) return match;
const value = rest.shift();
if (spec === 'c') return '';
if (spec === 'd' || spec === 'i') return String(Math.trunc(Number(value)));
if (spec === 'f') return String(Number(value));
return formatValue(value);
});
return [text, ...rest.map(formatValue)].join(' ');
}
export class ConsoleLog {
private buffer: LogLine[] = [];
private readonly listeners = new Set<() => void>();
private depth = 0;
constructor(private readonly now: () => number = Date.now) {}
/** Appends `text`; an embedded newline starts a new line, so a stack stays readable. */
/**
* Appends `text`; an embedded newline starts a new line, so a stack stays readable. Lines inside
* a group are indented under its head.
*/
push(level: LogLevel, text: string): void {
const time = this.now();
for (const part of text.split('\n')) this.buffer.push({ level, text: part, time });
const indent = ' '.repeat(this.depth);
for (const part of text.split('\n')) this.buffer.push({ level, text: indent + part, time });
if (this.buffer.length > LOG_CAPACITY) this.buffer = this.buffer.slice(-LOG_CAPACITY);
for (const listener of this.listeners) listener();
}
/** A group's head is a line like any other; what follows until `closeGroup` sits under it. */
openGroup(text: string): void {
this.push('info', text);
this.depth += 1;
}
closeGroup(): void {
this.depth = Math.max(0, this.depth - 1);
}
get lines(): readonly LogLine[] {
return this.buffer;
}
clear(): void {
this.depth = 0;
if (this.buffer.length === 0) return;
this.buffer = [];
for (const listener of this.listeners) listener();
@@ -72,29 +105,36 @@ export class ConsoleLog {
export const consoleLog = new ConsoleLog();
const METHODS = ['log', 'info', 'debug', 'warn', 'error'] as const;
const METHODS = ['log', 'info', 'debug', 'warn', 'error', 'group', 'groupCollapsed', 'groupEnd'] as const;
type ConsoleMethod = (typeof METHODS)[number];
export type ConsoleLike = Record<ConsoleMethod, (...args: unknown[]) => void>;
const LEVEL_OF: Record<ConsoleMethod, LogLevel> = {
const LEVEL_OF: Record<ConsoleMethod, LogLevel | 'open' | 'close'> = {
log: 'info',
info: 'info',
debug: 'info',
warn: 'warn',
error: 'error',
group: 'open',
groupCollapsed: 'open',
groupEnd: 'close',
};
/**
* Mirrors every `console.*` call into `log` and keeps calling the original, so devtools and the
* check scripts still see it. Returns the undo, which only the tests use.
* check scripts still see it. Groups are mirrored too: Pixi's deprecation warnings put the message
* into `groupCollapsed` and only the stack into `warn`. Returns the undo, which only the tests use.
*/
export function captureConsole(target: ConsoleLike = console, log: ConsoleLog = consoleLog): () => void {
const originals = METHODS.map((method) => [method, target[method]] as const);
for (const [method, original] of originals) {
const level = LEVEL_OF[method];
target[method] = (...args: unknown[]) => {
original.apply(target, args);
log.push(LEVEL_OF[method], formatArgs(args));
if (level === 'close') log.closeGroup();
else if (level === 'open') log.openGroup(formatArgs(args));
else log.push(level, formatArgs(args));
};
}
return () => {