All docs
Engine Docs Synced doc Engine source of truth

Engine Overview

High-level engine layout, current core surfaces, and repository ownership.

Generated from slicecore/splitframe View source doc

Note: This page is generated from the slicecore/splitframe engine repository. Edit the source document there and let automation sync it into splitframe-io.

Splitframe is being built as a native-first game engine with a Python gameplay layer, a deterministic content pipeline, and a first-class extension surface for plugins, mods, and future tooling.

The engine is strongest today as a 2D runtime, but the long-term target is broader than “another 2D framework.” The target state is:

Current support is narrower than that target: a Python gameplay layer, deterministic content contracts, a curated public API, supported examples, and the native Vulkan 2D renderer path described in docs/SUPPORTED_SURFACE.md. Items beyond that boundary are direction, not a claim that Splitframe already supports them.

  • native host ownership of frame lifecycle, device lifecycle, presentation, and recovery
  • Python-level iteration speed for gameplay, orchestration, and project logic
  • deterministic cooked-content and shader/runtime contracts
  • capability-gated plugin and modding boundaries
  • editor and tooling support built on the same engine data model
  • future renderer/runtime expansion only after the supported 2D core is stable
  • disciplined cross-platform packaging rather than many divergent runtimes

This document describes the actual engine shape that supports that direction.

Scope and Roadmap: three layers

The README’s “smaller, working core” and the broad forward-looking tracks in docs/ are not in conflict. They are different layers, and keeping them distinct is deliberate:

  1. Supported core — the smaller, working engine: the Python gameplay layer, deterministic content contracts, the curated splitframe.public API, supported examples, and the native Vulkan 2D renderer path. This is what Splitframe claims to support; it is recorded in SUPPORTED_SURFACE.md and gated by Tier 1–3 tests.

  2. Aspirational / spec-ready tracks — the forward-looking docs/*_TRACK.md documents (the 2D renderer push, primitive/advanced 3D, the native 3D execution lane, commercial render-demo recreation, …). These define value-intent, contracts, and acceptance criteria for direction. A [done] in those tracks means the spec and its focused tests are complete — it does not move the item into the supported core. Those focused tests are Tier 4 backstop coverage (TEST_STRATEGY.md).

  3. Quarantine candidates — unsupported or superseded code kept in-repo to preserve design knowledge until it is promoted, rewritten, or deleted. The map of what lives here is ASPIRATIONAL_SYSTEMS.md.

The rule that ties the layers together: breadth in docs/ is direction, gated behind a stable supported core; it is never a claim that Splitframe already supports those systems. Promotion from layer 2 or 3 into layer 1 follows the discipline in TEST_STRATEGY.md — a supported example, focused Tier 1–3 coverage, documentation in the supported surface, and a small public API.

Core Architectural Stances

The runtime host is authoritative

Splitframe does not treat the frame loop or renderer as an implementation detail hidden behind gameplay code. The native host owns:

  • frame begin/update/render/present lifecycle
  • renderer health, device loss, and recovery signaling
  • runtime profile application and policy enforcement
  • presentation and native backend compatibility checks

In practice that means the gameplay layer is important, but it is not the authority for runtime behavior.

Contracts beat convention

The engine is designed around explicit boundaries:

  • cooked content and runtime bundle validation
  • shader bundle activation and compatibility
  • backend compatibility and schema checks
  • plugin capability and dependency checks
  • versioned payloads between Python and native code

If two parts of the system need to cooperate, the preferred shape is a visible contract that can be validated and tested.

Code-first, not tooling-hostile

Splitframe should remain usable, testable, and automatable directly from code. That is not in conflict with editor ambitions. The intended model is:

  • code remains a first-class way to build and test projects
  • tooling and editor surfaces are built on top of engine-owned formats and services
  • the editor is a client of the engine model, not a separate source of truth

Extensions are product boundaries

Plugins and mods are not meant to rely on arbitrary engine internals. The engine already has the beginnings of a stable script ABI, manifest-based plugin loading, capability gates, and sandbox checks. The long-term goal is to turn that into a real extension platform.

The import surface is curated

splitframe.public is the curated long-term import surface. The root splitframe package exposes metadata only and no longer mirrors public symbols through compatibility exports. splitframe.public is organized around a few stable concept groups:

  • runtime boot and scene/entity orchestration
  • content/runtime contract types
  • plugin and modding boundaries

Most other subsystem symbols were removed from root re-exports. Supported subsystems remain accessible via their owning modules (e.g. from splitframe.tilemap import Tilemap). Experimental subsystem families are kept out of the documented import surface until they have supported examples and tests.

Current Engine Shape

The most important runtime surfaces live in:

  • splitframe/core.py
  • splitframe/scene/
  • splitframe/_system/
  • splitframe/entity.py
  • splitframe/content/
  • splitframe/rendering/
  • splitframe_api/
  • native/

Runtime boot and policy

splitframe/core.py is the main engine entry point. It applies a runtime profile during startup, requires cooked content when policy says it must, and activates the runtime shader bundle for native backends before normal gameplay execution begins.

For the native runtime, the supported default host path is:

  1. construct EngineConfig
  2. create GameEngine
  3. let the engine build the native startup manifest and backend-selection contract internally

Lower-level seams such as direct splitframe_native_renderer construction or raw backend-selection helpers still exist for tooling and contract tests, but they are not the preferred downstream boot story.

Type safety is improving but not finished. splitframe_api and splitframe_native_renderer are held to strict mypy settings. The native backend-selection/render-device path and splitframe.script_plugins plugin ABI module have also been promoted into explicit strict mypy islands. The rest of the main splitframe.* package still runs under a relaxed override because the salvaged core uses heavy mixin and adapter patterns. Runtime tests, capture validation, and boundary checks are therefore part of the current safety net until the core package can be tightened incrementally.

splitframe/runtime_profiles.py defines the profile-controlled environment policy for dev, potato, perf, and release. That is part of the product stance: production behavior is shaped intentionally rather than left to ad hoc environment toggles.

GameEngine composition

GameEngine (splitframe/core.py) is a composition root, not a monolith. It inherits eight single-concern mixins, each owning one slice of engine behavior:

MixinResponsibility
_CoreLifecycleMixinrun / quit / dispose / set_initial_scene, lifecycle state
_GameEngineLoopMixinper-frame update/render, render-context binding
_GameEngineSubsystemMixinsubsystem accessors, timing properties, lifecycle hooks
_GameEngineEventMixincanvas event handling and focus-loss auto-pause
_GameEngineDebugMixindebug/telemetry helpers
_GameEnginePluginMixinscript-plugin discovery and loading
_GameEngineExtensionMixinC++ extension hooks
_GameEngineDebugCaptureMixinheadless debug frame capture

Construction is a fixed, order-sensitive sequence of _init_* / _setup_* helpers in __init__; the ordering is load-bearing and documented in the constructor docstring.

The engine’s instance state is grouped by concern rather than left as loose attributes. The world-feature subsystems (lighting, time-of-day, weather, water, wall-occlusion, GPU-lighting) live together in the _EngineWorldSubsystems container (splitframe/_engine/world_subsystems.py); GameEngine exposes each through a public property (some with render-context sync side effects) that delegates to the container. The render subsystems (texture atlas, sprite batch, tile/ui renderers) are reached through the render context rather than stored directly on the engine.

Configuration is likewise navigable by concern: EngineConfig keeps its flat fields (and flat access) but also exposes read-only grouped viewsconfig.display, config.camera, config.tiles, config.timing, config.gpu, config.paths, config.behavior — so a subsystem can be handed just the slice it needs instead of the whole config (as GameEngine already does for input via splitframe.input.InputSettings).

Mixin governance. Because every mixin lands on the same composed class, a mixin that needs a sibling mixin’s state must reach it through a cast to a host Protocol, not a blanket # type: ignore[attr-defined]. tests/test_engine_mixins.py fails if that pattern regresses. The native render context follows the same philosophy; its mixin map and decomposition recipe are in RENDER_CONTEXT_MIXINS.md.

Native runtime and rendering

The engine currently treats native_vulkan as the authoritative runtime backend. Backend selection is not framed as a broad abstraction layer with many equal options. The intent is a strong native path with explicit compatibility validation and health signaling.

The Python/native boundary is also contract-oriented. Rendering submission, schema validation, and runtime compatibility checks live under splitframe/rendering/ and native/.

Native UI and primitive colors authored from Python are public sRGB RGBA values. The native bridge converts those colors to linear RGB before submission, the shader writes linear values, and the Vulkan swapchain presents through an sRGB surface when available. Pre-packed native instance buffers are lower-level data and are expected to already be in native linear color space.

For most engine consumers, that boundary should be reached through GameEngine, not by wiring native startup objects by hand. Direct native facade usage is the lower-level surface that engine bootstrap and smoke tooling exercise underneath the preferred host path.

Python/native frame-intent boundary

The architectural target for the runtime boundary is:

  • CPython constructs high-level, versioned intent for the next frame.
  • C++ executes that intent using native resources, worker threads, and GPU state.
  • Neither side reaches through the boundary to manipulate the other side’s live objects.

World submission follows this model through immutable FramePacket values. Frame packets carry schema versions, trace IDs, immutable payload bytes, and per-segment content hashes used by native diff caches. Native code may cache decoded native instances, stats, resource handles, and payload hashes; it must not retain Python-owned object pointers, memory addresses, bound-method identity, or mutable Python containers as frame identity.

Python may cache those immutable payload bytes and their computed content hashes between frames. That cache is still a Python-side value-intent cache: invalidation is based on Python-owned scene/chunk dirty state, and native receives only the versioned packet fields, payload bytes, and scalar hashes. Reusing a content hash is acceptable; reusing a Python object address as native frame identity is not.

The native host loop is a named control-plane exception. It may retain Python callback objects and invoke them on the host thread under the GIL so Python can build the next frame’s intent. Those callbacks are ingress points, not renderer data structures, and they must not be handed to worker threads or used as native resource identity.

Legacy incremental renderer calls still exist for UI command batches, resource uploads, lighting, animation state, post-processing, and camera updates. New performance work should move those surfaces toward immutable next-frame intent snapshots, following the FramePacket and lighting-intent shape rather than adding new direct live-state mutations.

Gameplay layer

Gameplay still lives comfortably in Python:

  • scenes and scene management
  • entities and state
  • input orchestration
  • game systems and progression logic
  • project-level bootstrap code

The stance is not “replace gameplay scripting.” The stance is to keep gameplay iteration fast without letting gameplay code implicitly own the runtime.

Systems and lifecycle

The private splitframe._system package provides the structured system manager with dependency and lifecycle ordering used by engine-owned subsystems. That is part of the engine’s modularity story: reusable subsystems should have explicit startup, update, and shutdown behavior instead of being wired together informally. Game projects should normally reach this through GameEngine rather than importing private system modules directly.

Content pipeline

splitframe/content/ owns deterministic asset manifests, cook graphs, cooked asset records, runtime bundle loading, and native shader bundle activation. The content pipeline is not an afterthought. It is part of how Splitframe intends to ship projects predictably.

Plugin and modding surfaces

splitframe_api/ defines the public script-facing ABI boundary between engine internals and Python-hosted gameplay or plugin modules.

splitframe/plugin_lifecycle.py, splitframe/script_plugins.py, and splitframe/modding_api.py provide the early foundation for:

  • manifest-based discovery
  • dependency-safe loading
  • capability-gated engine services
  • compatibility and version checks
  • extension lifecycle management

These surfaces are still early, but they are architecturally aligned with the engine’s target state.

Repository Ownership

This repository is the engine product, not a game workspace.

Engine-owned code includes:

  • splitframe/
  • splitframe_api/
  • splitframe_native_renderer/
  • native/
  • examples/simple_game/
  • docs/

Game-specific code belongs in downstream game repositories. Import boundaries are enforced so the engine does not depend back on game code.

What Is Still In Progress

Splitframe’s architecture is ahead of its polish in a few places.

Notable follow-up work includes:

  • making simple_game consume cooked content instead of constructing data procedurally, so the canonical example demonstrates the full cooked runtime path
  • removing raw-data fallbacks from production runtime paths and treating cooked bundles as the only release-path source of truth
  • continuing the path from strong 2D runtime architecture toward shared 2D/3D engine foundations
  • docs/BUILDING.md
  • docs/TESTING.md
  • docs/MIGRATION.md
  • native/README.md