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
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:
1 parent
82effb2c03
commit
0e5e5ef925
15 files changed
+328
-88
No files matched your search
@@ -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 () => {
|
||||
|
||||
Reference in new issue
Block a user