Skip to content

Debugging

This guide covers Estella’s runtime debugging tools, including error handling, playback control, the logger system, and editor debugging panels.

Error Handling

System Error Handling

When an ECS system throws an exception, the engine prints the error to the console and continues running by default. Register a handler with app.onSystemError() to control the post-error behaviour — return 'continue' to keep executing subsequent systems, or 'pause' to skip all remaining systems for the current frame:

app.onSystemError((error: Error, systemName?: string) => {
console.error(`System "${systemName}" failed:`, error.message);
if (error.message.includes('critical')) {
return 'pause';
}
return 'continue';
});

After returning 'pause', systems that have not yet run in the current frame are skipped, but the render loop does not stop. All systems resume normal execution at the start of the next frame.

WASM Error Handling

The C++ engine core runs via WebAssembly. Catch errors from the WASM layer with app.onWasmError():

app.onWasmError((error: unknown, context: string) => {
console.error(`WASM error in ${context}:`, error);
});

The context parameter identifies the WASM operation that failed (e.g. rendering, physics), making it easier to locate the source of the problem.

Playback Control

The SDK provides methods to control game execution at runtime, useful for frame-by-frame debugging and slow-motion analysis.

Pause and Resume

app.setPaused(true); // Pause: systems stop, only Last schedule (rendering) runs
app.setPaused(false); // Resume normal execution
app.isPaused(); // Query current pause state

While paused, the engine still runs the render loop (Schedule.Last), so the screen stays on the last rendered frame.

Single-Frame Stepping

While paused, call stepFrame() to advance exactly one frame — a full system schedule runs then pauses again:

app.setPaused(true);
app.stepFrame(); // Execute one full frame of system scheduling

Play Speed

Adjust time flow with setPlaySpeed(). The valid range is 0.1 to 4.0:

app.setPlaySpeed(0.25); // Quarter speed — good for observing fast animations
app.setPlaySpeed(2.0); // Double speed
app.getPlaySpeed(); // Get the current speed multiplier

Play speed works by scaling each frame’s delta time. All systems that read Time.delta are affected automatically.

Logger System

The SDK provides a centralized logging system with level filtering and custom handlers.

LogLevel

import { LogLevel, setLogLevel } from 'esengine';
// Debug = 0, Info = 1, Warn = 2, Error = 3
setLogLevel(LogLevel.Debug); // Output all levels
setLogLevel(LogLevel.Warn); // Output Warn and Error only

Using the Logger

Log functions accept a category tag, a message, and optional additional data:

import { debug, info, warn, error } from 'esengine';
debug('Physics', 'Collision detected', { entityA: 1, entityB: 2 });
info('Scene', 'Level loaded');
warn('Asset', 'Texture not found, using fallback');
error('Render', 'WebGL context lost');

The default ConsoleLogHandler formats output to the browser console:

[14:32:05.123] [INFO ] [Scene] Level loaded

Custom LogHandler

Implement the LogHandler interface to send logs to a custom destination (remote service, file, UI panel, etc.):

import { getLogger, type LogHandler, type LogEntry } from 'esengine';
const customHandler: LogHandler = {
handle(entry: LogEntry) {
// entry.timestamp, entry.level, entry.category, entry.message, entry.data
myRemoteService.send(entry);
}
};
const logger = getLogger();
logger.addHandler(customHandler);
// Remove a handler
logger.removeHandler(customHandler);

Editor Debugging

Output Panel

The editor’s Output panel captures all runtime console output:

  • Timestamps — each log entry includes millisecond-precision timing
  • Type filtering — filter by info, warn, or error
  • Search — search for keywords within log text

Game View Playback Controls

The editor’s Game View panel provides a graphical playback control toolbar:

  • Play / Pause — start or pause the game preview
  • Step — advance a single frame while paused
  • Speed — adjust playback speed

These controls call the setPaused(), stepFrame(), and setPlaySpeed() APIs described above.

Frame Debugger

The Frame Debugger lets you capture a single frame and inspect every draw call — useful for diagnosing batching issues, unexpected flush reasons, and render order problems.

Editor Panel

In the editor, open the Frame Debugger panel (available as a detachable window). Click Capture to freeze the current frame. The panel displays each draw call with its type, texture, material, triangle count, flush reason, and stencil/scissor state. Click a draw call to replay rendering up to that point and see the intermediate result.

SDK API

You can also capture and analyze frames programmatically:

import { Renderer, FlushReason, RenderType } from 'esengine';
// Trigger capture (takes effect next frame)
Renderer.captureNextFrame();
// After the frame renders, retrieve the captured data
const capture = Renderer.getCapturedData();
if (capture) {
for (const dc of capture.drawCalls) {
console.log(
`#${dc.index} ${RenderType[dc.type]} | ` +
`${dc.triangleCount} tris | ` +
`flush: ${FlushReason[dc.flushReason]}`
);
}
}
// Replay to a specific draw call for visual inspection
Renderer.replayToDrawCall(3);
const snapshot = Renderer.getSnapshotImageData();

FlushReason

Why the renderer started a new draw call:

ReasonDescription
BatchFullVertex buffer capacity reached
TextureSlotsFullAll texture slots occupied
ScissorChangeScissor rect changed (UIMask)
StencilChangeStencil state changed
MaterialChangeDifferent material/shader
BlendModeChangeBlend mode changed
StageEndRender stage boundary
TypeChangeDifferent render type (sprite → text)
FrameEndEnd of frame flush

RenderType

TypeDescription
SpriteSprite rendering
TextBitmap text
SpineSpine skeleton
ParticleParticle emitter
ShapeShape renderer
UIElementUI element
MeshCustom mesh

WASM Debugging

The engine core runs as WebAssembly. WASM debugging requires a debug build of the engine.

Debug Build

Build the engine in debug mode:

Terminal window
node build-tools/cli.js build -t web -d

This produces an unoptimized WASM binary with DWARF debug info. The binary is significantly larger than a release build.

Chrome WASM Debugging

  1. Install the C/C++ DevTools Support Chrome extension
  2. Open Chrome DevTools
  3. Navigate to the Sources panel
  4. WASM source files appear under the file:// tree
  5. Set breakpoints in C++ source files and step through code

Performance Profiling

Frame Time

Use the browser Performance panel to record a trace:

  1. Open DevTools and go to the Performance tab
  2. Click Record, interact with your game, then stop
  3. Examine the flame chart for long frames

Look for:

  • Long system execution — a single system taking too much time per frame
  • Asset loading stalls — large textures or Spine data blocking the main thread
  • GC pauses — frequent garbage collection from excessive allocations

Draw Call Count

Use the Draw API to monitor render statistics:

import { Draw } from 'esengine';
console.log('Draw calls:', Draw.getDrawCallCount());
console.log('Primitives:', Draw.getPrimitiveCount());

ECS Query Performance

If a system with a broad query runs slowly, narrow the query by adding more specific component filters. Queries with fewer matching entities run faster.

const narrowSystem = defineSystem(
[Query(Mut(Transform), Velocity, Player)],
(query) => {
for (const [entity, transform, velocity] of query) {
transform.position.x += velocity.linear.x;
}
}
);

Common Errors

”WASM module failed to compile”

Cause: The WASM binary is corrupted or incompatible with the current browser.

Fix:

  • Rebuild the WASM binary: node build-tools/cli.js build -t web -c
  • Verify the Emscripten version matches what the project expects
  • Check that the browser supports WebAssembly

”Entity not found” / Invalid entity access

Cause: Accessing an entity that has been despawned, or using a stale entity ID.

Fix:

  • Check entity existence before access: if (world.isAlive(entity)) { ... }
  • Avoid storing entity IDs across frames without validation
  • Remember that Commands are deferred — entities are not despawned until the end of the schedule stage

”Component not registered”

Cause: Using a component type that has not been defined or imported.

Fix:

  • Ensure defineComponent() or defineBuiltin() is called before using the component
  • Check that the component module is imported in your entry file
  • For builtin components, verify the SDK version matches the WASM binary version

”WebGL context lost”

Cause: The GPU driver reset the WebGL context, usually due to resource exhaustion.

Fix:

  • Reduce texture memory usage (smaller atlases, compressed textures)
  • Reduce the number of active render targets
  • On mobile devices, minimize background GPU usage

Blank or black screen in preview

Cause: Multiple possible causes.

Checklist:

  1. Check DevTools console for errors
  2. Verify the scene file exists and is valid JSON
  3. Ensure at least one camera entity exists in the scene
  4. Check that textures referenced by sprites are loaded (no 404s in Network tab)
  5. Verify the WASM binary loaded successfully

Build output does not match editor preview

Cause: Build-time asset processing (atlas packing, material compilation) can surface issues not visible in preview.

Fix:

  • Compare the DevTools console output between preview and build
  • Verify all assets are included (check asset-manifest.json)
  • For WeChat builds, check that custom file extensions are in packOptions.include

Next Steps