Terminal-only is still the default. S6 connects the existing S2 transport, S3 input, S4 presentation and S5 sprite capabilities to the real PHP Game loop. No renderer is started merely because a project authors graphical sprites.
Console selects a stable public renderer ID and sends it to the ordinary PHP
game entrypoint in ICHILOTO_RENDERER. Current IDs are exactly terminal and
gpui. The engine reads the value once while resolving initial geometry, trims
whitespace and lowercases it. An absent, empty or whitespace-only value means
terminal, so direct php game.php launches retain existing behavior.
ichiloto play --renderer=terminal
ichiloto play --renderer=gpui
ichiloto play --gpui-rendererichiloto play in an interactive terminal offers Native Terminal and GPUI.
Console communicates only the selected ID, including for new tmux sessions;
the engine does not know which flag or prompt produced it. No renderer binary
path is accepted by this public contract. Reattaching an existing tmux session
does not restart that game or change its renderer.
An ordinary project bootstrap is sufficient:
use Ichiloto\Engine\Core\Game;
(new Game('My game'))->run();Rendering\Launch\RendererRegistry maps IDs to immutable descriptors with
runtime factories. Terminal returns no external runtime and does not inspect
renderer packages. GPUI resolves an installed implementation and constructs
RendererProcessConfig, RendererRuntimeConfig and RendererRuntime internally.
Its registration uses 10x20 pixel cells, the existing v2 protocol, and the
project's canonical assets directory under the launch working directory.
Logical dimensions come from the Game, not from Console or binary discovery.
Since S8-A the automatic GPUI registration requires the negotiated
sprite_source_rect capability. An older installed renderer fails startup clearly
rather than displaying an entire sprite sheet. Rebuild/install the matching
renderer when updating this spike. Explicit programmatic runtime configurations
retain an empty requirement list by default for legacy full-image integrations;
sheet users must request the capability as described in sprite sheets.
S8-B additionally requires v2 tile_batches for automatic GPUI startup, even
when the initial map has no graphical terrain. This permits later transfers
without renegotiation. An older binary must fail clearly before frames are sent.
See optional map terrain and the wire contract and the
S8-B validation/publication gate before installing this slice.
PackagedRendererExecutableResolver is the sole owner of the installation
manifest layout and platform lookup. It resolves only a readable installed
manifest entry naming an executable within that package. See the
internal packaging boundary for packager
and test details. This is not project gameplay configuration, executable search
on PATH, a binary download, or a public renderer-path environment variable.
Unknown IDs fail with the supplied ID and the valid IDs. An unavailable GPUI
package, unsupported installation/platform, or invalid manifest raises
RendererUnavailableException with a renderer-availability explanation.
Neither case silently falls back to terminal. Normal Game startup routes these
errors through its existing crash log and notice.
An explicitly attached Game::useRendererRuntime() is authoritative, even if
the environment contains another or invalid ID. Otherwise the engine resolves
launch intent; absent intent means terminal. Selection precedes runtime startup
and the decision to claim STDIN raw/nonblocking modes. No second runtime is
created for an explicitly attached session.
$game->useRendererRuntime($embeddedRuntime)->run();This API remains for tooling, tests, custom embedding and the unchanged temporary
Last Legend spike launcher. One runtime can be attached before input startup and
owns one session. Tests can inject a RendererRegistry into Game, descriptor
factories into the registry, or a RendererExecutableResolverInterface into the
default registry. Ordinary unit tests need no installed Rust binary.
The graphical runtime defaults to protocol v2. Pass protocol: RendererProtocolVersion::V1 to RendererRuntimeConfig for the retained v1 path;
this does not change the default for low-level transport sessions. Existing
launchers that do not pin a protocol gain v2 without project source changes.
The Game's resolved logical dimensions become the session grid. Graphical
sessions default to BattleScreen::WIDTH by BattleScreen::HEIGHT: 135x36
cells, including the battlefield and its bottom controls. At the registered
10x20 cell pitch, that is a 1350x720 logical-pixel canvas. The main menu's
110x35 layout fits within the same surface. The launching terminal's dimensions
do not change this default, and switching scenes does not resize the grid.
Explicit flat or nested width/height options are honored per axis, even when equal to legacy constructor defaults. Non-default positional dimensions also remain supported. A dimension left on auto uses the battle footprint for a graphical session and the capped available dimension for a terminal session. Caller requests are retained separately from resolved dimensions, so attaching a runtime after construction does not turn terminal measurements into explicit graphical overrides. All registered camera viewports are synchronized before the renderer handshake, including scenes that have not started yet.
Explicit smaller grids can still clip authored layouts inside Console; an 80x30 override cannot contain the battle UI. Cell dimensions change display size, not the number of available layout cells. The physical GPUI window remains resizable and scales/centers this fixed canvas rather than changing its grid. Maps larger than the viewport continue to scroll through the PHP-owned Camera.
The graphical grid is fixed until the session ends. Later terminal resizing does not change Game, Camera or protocol geometry. Terminal-only sessions use the bounded dynamic resize path below. Neither protocol negotiates a new logical grid on resize; GPUI's existing viewport fitting is presentation-only.
Native terminal sessions are limited to 135x36 cells, using the same
BattleScreen dimensions as the graphical default. Each axis is the minimum
of the caller's resolved request, the physical terminal dimension and the battle
dimension. This cap applies to explicit terminal sizes too; smaller requests
remain effective through terminal shrink/regrow cycles. Graphical overrides
are not capped by this terminal policy.
Startup and the existing throttled resize check use the same resolution rule. Only changes to the effective grid reset the buffer, update all camera viewports and notify the current scene. Resizing between two larger physical terminals does not trigger those operations. The engine does not resize the terminal window to match its logical drawing area; unused terminal space stays unused. Low-level physical size probes still return the actual terminal dimensions.
The complete logical viewport, including empty cells, is centered on both axes.
Its zero-based physical origin is floor(max(0, physical - logical) / 2) per
axis. A 135x36 grid in a 186x38 terminal therefore starts at (25, 1), leaving
25 columns on the left, 26 on the right and one row above and below. Only the
terminal output boundary applies this origin; buffers, Cameras, coordinates and
graphical snapshots stay logical. Direct writes, batched frames and legacy
one-based absolute Cursor operations share the same translation.
A physical resize clears stale margins and replays the canonical screen even when the capped logical dimensions or rounded origin stay unchanged. It uses the existing quarter-second size probe, not additional probes per draw. Blocked dialogue/timer frames also refresh margins once composition has finished, without re-entering scene updates. Logical resizing waits for normal gameplay to resume while a blocking operation owns the layout; shrinking the physical terminal below that retained layout can temporarily clip it. See terminal centering validation.
Maps larger than this area scroll normally. A physical terminal smaller than 135x36 uses its available space, but cannot display the complete fixed battle layout; the cap does not add scaling or a small-screen layout. See the terminal cap validation for measured composition costs and platform limitations.
Game construction is buffer-only. At startup, after renderer selection and any explicit runtime attachment, Game selects physical Console output independently of input ownership. Native terminal sessions enable terminal output; external renderer sessions keep canonical cells and named layers but do not mirror frames, emit terminal controls, open a terminal output descriptor, or probe emoji with cursor-position queries. Errors still reach the existing logs and stderr notice.
Console::setTerminalOutputEnabled() must run before a frame or alternate screen
is active. Buffer-only mode skips terminal dirty-span comparisons and payload
assembly, not composition, rollback, snapshots, colours, or sprite exclusion.
This applies equally to GPUI and future external runtimes. Shutdown does not
reenable terminal painting; a later terminal Game selects its own output normally.
Styled cells retain active SGR attributes rather than a growing history of obsolete colour assignments. Selective resets preserve unrelated attributes; extended colour components remain grouped. Unknown controls retain their prior replay behaviour until a full reset. See Garden validation for the measured dense-map impact and test boundaries.
Field\PlayerPresentationConfig::load() reads the existing
assets/Data/Entities/player.php. Its sprites key remains terminal art. Its
optional sprites2d key uses the exact
DirectionalGraphicalSpriteSet::fromArray() format documented in
graphical sprites. Missing means no graphical set;
present but malformed data fails clearly, including explicit null.
The alternative mode: sheet structure is documented in sprite sheets.
New-game loading uses the same project presentation loader for terminal art. GameScene's shared Player construction path loads current graphical definitions for both new and restored games. Position, heading and saved terminal fields still come from GameConfig. No graphical definitions enter save serialization, and changing artwork requires no save migration.
GraphicalSpriteProviderHostInterface is optional, not a new requirement on
every SceneInterface. The collector asks the active host for providers, ignores
null definitions and uses the existing S5 projector and PHP Camera. IDs must be
unique; the field Player is always player.
GameScene exposes the active Player while its FieldState owns presentation. Ordinary event dialogue borrows input without replacing the field, so the PNG remains visible during dialogue. This also applies to safe blocked-timer ticks. Title, menu, item, records, controls, map, shop and battle screens receive no field sprite. Active cinematics remain conservatively text-only in S6.
Off-grid providers still reach GPUI for protocol clipping. Snapshot masking is limited to providers whose projected anchor is inside the logical grid; an off-grid provider cannot accidentally mask a terminal edge cell. No second world visibility or camera algorithm is introduced.
Normal Player rendering still writes terminal art. Its ordinary draw runs
inside Console::withLayer('player', ...). Optional Console layer tracking
records the existing canonical cells below that draw. The actual field
compositor remains the sole source of map, event-cue and NPC ordering.
Console::presentationSnapshot(['player']) in v2, or Console::snapshot(['player'])
in v1, creates a separate immutable snapshot excluding
that named layer. It restores recorded underlay, not unconditional spaces,
using Console's existing wide-cell representation. Multi-row and wide glyphs
retain their footprints. Later ordinary writes invalidate provenance at the
cells they overwrite, even if they write the same glyph, so masking preserves
later overlays. Recomposition rollback restores provenance with the buffer;
clear/resize/recomposition discard stale history. Retained history is bounded
by the grid and named layers, not the number of frames.
Only the optional runtime enables this tracking. Capturing a snapshot neither draws nor changes Console output, dirty state or gameplay. S4 still rejects incomplete frames and owns duplicate suppression and frame numbering.
V2 additionally preserves structured foreground/background colour and sparse
named UI layers. PresentationLayerPolicy reserves world text at 0, world
sprites at 0..999, ordinary UI at 1000, existing FIELD_HUD at 1010 and MODAL at
1020, notifications at 2000, and transition cover at 3000. Automatic runtime
composition rejects sprites outside the world range; the generic sprite DTO
still permits the protocol's signed i32 layer range. Explicit UI spaces paint
opaque cells above sprites; missing UI cells are transparent. See
styled presentation for authoring and extraction rules.
One RendererClient is shared by RendererInputSource and RendererPresentation. Game pumps lifecycle, polls PHP input, updates PHP simulation, renders the normal terminal composition, then presents its snapshot and providers. Rust receives no movement, collision, event, heading, camera or save authority.
After enqueueing a changed frame, Runtime performs one bounded, zero-wait I/O pass before returning to Game/Timers' sleep. A writable small frame begins delivery in the same iteration; backpressure or a frame larger than the I/O budget retains pending bytes for later pumps. This is not an acknowledgement or synchronous wait for native drawing. Unchanged frames do not trigger that extra pass.
InputManager::requiresTerminalInput() centralizes input-mode ownership. GPUI
input does not claim STDIN raw/no-echo/nonblocking modes. Terminal input keeps
its existing setup. Cleanup restores only input modes the Game actually claimed
and reinstalls the previous input source when the renderer still owns it.
Blocked waits update lifecycle/timers/audio/notifications, then let an optional caller draw, then present the completed Console frame before sleeping. They do not update the scene recursively. Native close is a persistent lifecycle signal that unwinds waits into ordinary Game quit without an input binding or prompt. Renderer errors and transport failures reach the existing crash log/notice path. Explicit Game cleanup shuts down audio and the renderer on normal quit, native close, exceptions, PHP shutdown and supported signal paths. Cleanup is idempotent and reuses S2's bounded process shutdown rather than relying on destructors.
Set ICHILOTO_ENGINE_TRACE=1 when launching the ordinary CLI to append NDJSON
observations to the private project's logs/latency.ndjson. Leave it unset for
normal play. Tracing uses hrtime(true), not wall time, and changes no protocol
fields. Records identify the PHP process, iteration, input identity and stage.
Input records contain key identities, so treat the log as diagnostic data.
The internal Diagnostics\LatencyTrace observes transport parse/queue/dequeue,
source return, input acceptance/KeyboardEvent dispatch, update/render boundaries,
snapshot/style/run costs, sprite collection, payload comparison, JSON encoding,
frame enqueue and byte draining. Queue depth/oldest observed age and per-iteration
consumption counts are diagnostic only. Ages begin at PHP's first observation,
not physical key-down. Synthetic transports have no native parse timestamp.
Records are buffered with a 4096-record cap and flushed to the log at frame/wait
boundaries; dropped records are marked. Logging still has observer overhead and
must be considered during native comparisons. Nothing is written to normal stdout.
LatencyTrace::configure() supplies internal test sinks/clocks, not a gameplay API.
Renderer-relative Instant timestamps cannot simply be subtracted from PHP
hrtime. Native-to-PHP measurements require a matching monotonic-clock anchor
and an explicit uncertainty bound. Current native acceptance status and exact
evidence are in S7-E validation.
The logical game surface, native terminal dimensions, and graphical renderer viewport are different concepts. GPUI resize is presentation-only: it must not change Console dimensions, Camera geometry or the session grid. Terminal mode caps its existing size-probe results as described above. Larger maps still scroll through the PHP-owned Camera rather than becoming larger protocol grids automatically.
Future configuration may select logical resolutions, preferred window size, resizability, scaling policy or fullscreen independently. Current renderer defaults are not permanent restrictions on developers/players. None of those preferences, resize messages or public window-preference APIs is implemented by this follow-up.
- Only the field Player is graphical. NPCs, objects, maps, battles and UI remain terminal presentation. S8-A's example replaces the earlier Player calibration placeholders with author-supplied directional sheets.
- Explicit protocol v1 flattens text and draws sprites afterwards. V2 fixes world/sprite/UI ordering and opaque UI blanks. Cinematics remain text-only.
- GPUI fits text to measured font advance and line metrics inside fixed cells. This supersedes the earlier undersized-font limitation without changing grid dimensions, sprite geometry or viewport fitting. Very small viewports still reduce the entire surface; fitting cannot guarantee legibility at every size.
- V2 preserves colours, not blink, bold weight, italic, underline or other terminal attributes. V1 remains unstyled.
See S6 validation and S7-E validation for measured acceptance status.