This document describes how to properly layer the HFSM-based application lifecycle across ork.core and ork.lev2 to support both:
- Server-side applications (core only, no graphics)
- GUI applications (lev2, with graphics/audio/UI)
Key Principle: Basic lifecycle (queues, threads, subsystems, FSM) in ork.core, extended by ork.lev2 for GPU/Audio/UI.
Related Documents:
- apphfsm.md - Complete HFSM application lifecycle design
| Component | Location | Issue |
|---|---|---|
| AppFsm | lev2 | ❌ Requires graphics layer |
| Subsystem coordination | lev2 | ❌ Can't use for servers |
| OPQ management | lev2/EzApp | ❌ Coupled to GUI |
| UPDATE thread | lev2/EzApp | ❌ Coupled to rendering |
Problem: Server-side apps (using ork.core only) have no standard lifecycle framework.
| Component | Location | Benefit |
|---|---|---|
| Application class | ork.core | ✅ Server apps can use |
| AppFsm (core states) | ork.core | ✅ No graphics dependencies |
| Subsystem coordination | ork.core | ✅ Reusable everywhere |
| OPQ management | ork.core/Application | ✅ Standard queue lifecycle |
| UPDATE thread | ork.core/Application | ✅ Decoupled from rendering |
| OrkEzApp extends Application | ork.lev2 | ✅ Adds GPU/Audio/UI |
| GPU/Audio subsystems | ork.lev2 | ✅ Optional, not required |
ork.core/inc/ork/application/
├── application.h (NEW: Application base class with FSM lifecycle)
├── appfsm.h (NEW: Core AppFsm without GPU/Audio states)
├── subsystem.h (NEW: Subsystem base class)
└── (existing files unchanged)
ork.core/src/application/
├── application.cpp (NEW: Core lifecycle implementation)
├── appfsm.cpp (NEW: Build core FSM states)
├── subsystem.cpp (NEW: Subsystem coordination)
└── application.cpp (EXISTING: AppInitData, unchanged)
// ork.core/inc/ork/application/application.h
namespace ork {
// Forward declarations
class Application;
class Subsystem;
struct SubsystemRegistration;
using application_ptr_t = std::shared_ptr<Application>;
using subsystem_ptr_t = std::shared_ptr<Subsystem>;
using subsystem_reg_ptr_t = std::shared_ptr<SubsystemRegistration>;
///////////////////////////////////////////////////////////////////////////////
// Application - Base class for all Orkid applications
// Provides FSM-based lifecycle without graphics dependencies
// Uses static factory pattern (no shared_from_this)
///////////////////////////////////////////////////////////////////////////////
class Application {
public:
// Static factory pattern (returns shared_ptr)
static application_ptr_t create(appinitdata_ptr_t initdata);
virtual ~Application();
// Main entry point
virtual void mainThreadLoop();
// Subsystem management (thread-safe, dynamic)
// Dependencies must be set in subsystem->_dependencies before calling
void registerSubsystem(
subsystem_ptr_t subsystem,
bool is_static = false
);
// Convenience overload for string names (auto-hashes to uint64_t)
void registerSubsystem(
const std::string& name,
subsystem_ptr_t subsystem,
bool is_static = false
);
void unregisterSubsystem(const std::string& name);
subsystem_ptr_t findSubsystem(const std::string& name) const;
bool isSubsystemRunning(const std::string& name) const;
// Query
size_t subsystemCount() const;
std::vector<std::string> subsystemNames() const;
// Shutdown
virtual void signalExit();
// Access
fsm::fsminstance_ptr_t getFsm() const { return _app_fsm; }
opq::opq_ptr_t getMainQueue() const { return _mainq; }
opq::opq_ptr_t getUpdateQueue() const { return _updq; }
opq::opq_ptr_t getConcurrentQueue() const { return _conq; }
protected:
// Constructor (protected - use factory)
Application(appinitdata_ptr_t initdata);
// === VIRTUAL HOOKS FOR DERIVED CLASSES ===
// Build FSM - override to add states (call base first!)
virtual void buildApplicationFsm();
// Lifecycle extension points (called from FSM state callbacks)
virtual void onAppInitExtension() {}
virtual void onUpdateExtension() {}
virtual void onShutdownExtension() {}
// Subsystem helpers (derived classes can override)
virtual void initStaticSubsystems();
virtual void shutdownStaticSubsystems();
// === PROTECTED MEMBERS (accessible to derived classes) ===
appinitdata_ptr_t _initdata;
// FSM
fsm::fsminstance_ptr_t _app_fsm;
fsm::fsmdata_ptr_t _fsm_data;
// Core states (cached references)
fsm::state_ptr_t _state_uninitialized;
fsm::state_ptr_t _state_initializing;
fsm::state_ptr_t _state_app_init;
fsm::state_ptr_t _state_update_init;
fsm::state_ptr_t _state_running;
fsm::state_ptr_t _state_shutting_down;
fsm::state_ptr_t _state_exit_requested;
fsm::state_ptr_t _state_draining_queues;
fsm::state_ptr_t _state_joining_update;
fsm::state_ptr_t _state_final_cleanup;
fsm::state_ptr_t _state_terminated;
fsm::state_ptr_t _state_error;
// Operation queues (references to global singletons)
opq::opq_ptr_t _mainq;
opq::opq_ptr_t _updq;
opq::opq_ptr_t _conq;
// UPDATE thread
Thread _updateThread;
std::atomic<bool> _update_thread_running{false};
// Subsystem coordination
std::map<std::string, subsystem_reg_ptr_t> _registered_subsystems;
fsm::fsmgroup_ptr_t _subsystem_group;
mutable std::mutex _subsystem_mutex;
private:
// Core lifecycle helpers (non-virtual)
void _initAppCore();
void _initUpdateThread();
void _shutdownCore();
// Subsystem helpers
std::vector<subsystem_reg_ptr_t> _getIndependentSubsystems();
std::vector<subsystem_reg_ptr_t> _getReadyToInitSubsystems();
void _initSubsystem(subsystem_reg_ptr_t reg);
void _shutdownSubsystem(subsystem_reg_ptr_t reg);
void _buildShutdownWaves(
std::vector<std::vector<subsystem_reg_ptr_t>>& waves,
const std::vector<subsystem_reg_ptr_t>& subsystems
);
};
} // namespace orkAppLifecycle (Root)
├── UNINITIALIZED
├── INITIALIZING
│ ├── APP_INIT
│ │ ├─ Core: Initialize queues, string pool
│ │ ├─ Extension: onAppInitExtension() hook
│ │ └─ Init DURING_APP_INIT subsystems (network, databases)
│ └── UPDATE_INIT
│ ├─ Core: Spawn UPDATE thread
│ ├─ Init AFTER_UPDATE_INIT subsystems (game logic, ECS)
│ └─ Wait for all subsystems READY
├── RUNNING
│ ├─ Core: Process queues, tick subsystems
│ └─ Extension: onUpdateExtension() hook
├── SHUTTING_DOWN
│ ├── EXIT_REQUESTED
│ │ └─ Core: Disable queues (enqueues now no-op), signal UPDATE to exit
│ ├── JOINING_UPDATE
│ │ └─ Core: Join UPDATE thread
│ ├── DRAINING_QUEUES
│ │ ├─ Core: Drain all queues (process work enqueued before EXIT_REQUESTED)
│ │ └─ Extension: onShutdownExtension() hook (called AFTER queues drained)
│ └── FINAL_CLEANUP
├── TERMINATED
└── ERROR
Key: Core states handle queue management, threading, subsystem coordination. Extension hooks allow lev2 to add GPU/Audio logic.
// ork.lev2/inc/ork/lev2/ezapp.h
namespace ork::lev2 {
class OrkEzApp : public ork::Application {
public:
// Factory (follows Orkid pattern)
static ezapp_ptr_t create(appinitdata_ptr_t initdata);
~OrkEzApp() override;
// Override main loop (adds rendering)
void mainThreadLoop() override;
protected:
// Constructor
OrkEzApp(appinitdata_ptr_t initdata);
// === OVERRIDE FSM BUILDING ===
void buildApplicationFsm() override;
// === OVERRIDE LIFECYCLE HOOKS ===
void onAppInitExtension() override;
void onUpdateExtension() override;
void onShutdownExtension() override;
// === OVERRIDE SUBSYSTEM HOOKS ===
void initStaticSubsystems() override;
void shutdownStaticSubsystems() override;
private:
// === EzApp-SPECIFIC MEMBERS ===
// GPU subsystem
std::shared_ptr<GpuSubsystem> _gpu_subsystem;
ezmainwin_ptr_t _mainWindow;
CTXBASE* _ctqt;
// Audio subsystem
std::shared_ptr<AudioSubsystem> _audio_subsystem;
audiodevice_ptr_t _audiodevice;
audio::singularity::synth_ptr_t _synth;
// Render thread queue (lev2-specific)
opq::opq_ptr_t _rthreadq;
// EzApp states (added to core FSM)
fsm::state_ptr_t _state_gpu_init;
fsm::state_ptr_t _state_gpu_context_create;
fsm::state_ptr_t _state_gpu_resources_load;
fsm::state_ptr_t _state_audio_init;
fsm::state_ptr_t _state_synth_init;
fsm::state_ptr_t _state_audio_device_start;
fsm::state_ptr_t _state_gpu_cleanup;
fsm::state_ptr_t _state_audio_shutdown;
// EzApp lifecycle helpers
void _initGpu();
void _initAudio();
void _renderFrame();
void _shutdownGpu();
void _shutdownAudio();
// Add GPU/Audio states to FSM
void _addGpuStates();
void _addAudioStates();
// User callbacks (protected, for compatibility)
void_lambda_t _onGpuInit;
void_lambda_t _onAudioInit;
void_lambda_t _onGpuExit;
void_lambda_t _onAudioExit;
// ... etc
};
} // namespace ork::lev2EzApp extends the core FSM by inserting states into the INITIALIZING hierarchy:
void OrkEzApp::buildApplicationFsm() {
// 1. Call base to build core FSM
Application::buildApplicationFsm();
// 2. Insert GPU states between APP_INIT and UPDATE_INIT
_addGpuStates();
// 3. Insert Audio states after GPU
_addAudioStates();
// 4. Modify JOINING_UPDATE to add GPU/Audio cleanup
_addCleanupStates();
}
void OrkEzApp::_addGpuStates() {
// Create GPU hierarchy
_state_gpu_init = _fsm_data->createState(_state_initializing, "GPU_INIT");
_state_gpu_context_create = _fsm_data->createState(_state_gpu_init, "GPU_CONTEXT_CREATE");
_state_gpu_resources_load = _fsm_data->createState(_state_gpu_init, "GPU_RESOURCES_LOAD");
// Insert into transition chain: APP_INIT → GPU_INIT → AUDIO_INIT → UPDATE_INIT
_fsm_data->addTransition(_state_app_init, "APP_READY", _state_gpu_context_create);
_fsm_data->addTransition(_state_gpu_context_create, "CONTEXT_CREATED", _state_gpu_resources_load);
// Callbacks
_state_gpu_context_create->_onenter = [this](fsm::fsminstance_ptr_t inst) {
_initGpu(); // Create window, Vulkan context
inst->sendEvent("CONTEXT_CREATED");
};
_state_gpu_resources_load->_onenter = [this](fsm::fsminstance_ptr_t inst) {
// Core GPU resources
_loadCoreGpuResources();
// User callback (protected)
if (_onGpuInit) {
try {
_onGpuInit(_ctqt);
} catch (...) {
logchan_ezapp->log_error("User GPU init failed");
}
}
inst->sendEvent("GPU_READY");
};
}AppLifecycle (Root)
├── UNINITIALIZED
├── INITIALIZING
│ ├── APP_INIT (core)
│ │ └── Init DURING_APP_INIT subsystems
│ ├── GPU_INIT (EzApp adds) ← INSERTED HERE
│ │ ├── GPU_CONTEXT_CREATE
│ │ └── GPU_RESOURCES_LOAD
│ │ └── Init AFTER_GPU_INIT subsystems
│ ├── AUDIO_INIT (EzApp adds) ← INSERTED HERE
│ │ ├── SYNTH_INIT
│ │ └── AUDIO_DEVICE_START
│ │ └── Init AFTER_AUDIO_INIT subsystems
│ └── UPDATE_INIT (core)
│ └── Init AFTER_UPDATE_INIT subsystems
├── RUNNING (both use)
│ └── onUpdateExtension() renders frame
├── SHUTTING_DOWN
│ ├── EXIT_REQUESTED (core)
│ │ ├── Disable queues (enqueues no-op)
│ │ ├── Shutdown BEFORE_UPDATE_EXIT subsystems
│ │ └── Signal UPDATE to exit
│ ├── JOINING_UPDATE (core)
│ │ ├── _updateThread.join() ← UPDATE THREAD JOINS FIRST
│ │ └── Shutdown BEFORE_QUEUES_DRAIN subsystems
│ ├── DRAINING_QUEUES (core)
│ │ ├── Drain remaining work ← DRAIN AFTER UPDATE TERMINATED
│ │ └── onShutdownExtension() → calls:
│ │ ├─── Shutdown BEFORE_AUDIO_SHUTDOWN subsystems
│ │ ├─── AUDIO_SHUTDOWN (EzApp adds)
│ │ ├─── Shutdown BEFORE_GPU_CLEANUP subsystems
│ │ └─── GPU_CLEANUP (EzApp adds)
│ └── FINAL_CLEANUP (core)
│ └── Shutdown DURING_FINAL_CLEANUP subsystems
├── TERMINATED
└── ERROR
// ork.core/inc/ork/application/subsystem.h
namespace ork {
///////////////////////////////////////////////////////////////////////////////
// Subsystem - Base class for application subsystems
// Provides standard lifecycle states
///////////////////////////////////////////////////////////////////////////////
class Subsystem {
public:
Subsystem(const std::string& name);
~Subsystem() = default;
// Called by factory functions to set up FSM states
void initialize();
// Shutdown subsystem
void shutdown();
// Query
fsm::state_ptr_t currentState() const;
const std::string& name() const { return _name; }
// FSM access (protected members for derived classes)
fsm::fsminstance_ptr_t _instance;
fsm::fsmdata_ptr_t _data;
std::string _name;
// Standard subsystem states (cached for convenience)
fsm::state_ptr_t _state_uninitialized;
fsm::state_ptr_t _state_initializing;
fsm::state_ptr_t _state_ready;
fsm::state_ptr_t _state_shutting_down;
fsm::state_ptr_t _state_terminated;
fsm::state_ptr_t _state_error;
};
///////////////////////////////////////////////////////////////////////////////
// Note: Initialization and shutdown order is IMPLICIT from dependency graph
// No need for explicit init/shutdown phase enums - dependencies determine everything!
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
// SubsystemRegistration - Metadata for registered subsystems
///////////////////////////////////////////////////////////////////////////////
struct SubsystemRegistration {
uint64_t name_hash; // CRC hash of subsystem name
std::string name; // "gpu", "audio", "ecs" (for debugging)
subsystem_ptr_t subsystem;
bool is_static = false; // Static = persists until app exit
std::atomic<bool> is_initializing{false};
std::atomic<bool> is_shutting_down{false};
// Note: Dependencies stored in subsystem->_dependencies map
// Init/shutdown order is IMPLICIT from dependency graph
};
} // namespace ork// ork.lev2/inc/ork/lev2/gpu_subsystem.h
namespace ork::lev2 {
// GPU-specific implementation (pimpl)
struct GpuSubsystemImpl {
CTXBASE* _context = nullptr;
ezmainwin_ptr_t _window;
};
// GPU subsystem factory function
subsystem_ptr_t createGpuSubsystem();
// Audio-specific implementation (pimpl)
struct AudioSubsystemImpl {
audiodevice_ptr_t _device;
audio::singularity::synth_ptr_t _synth;
};
// Audio subsystem factory function
subsystem_ptr_t createAudioSubsystem();
} // namespace ork::lev2The Catalog subsystem demonstrates the graceful drain pattern for subsystems with async operations:
// ork.core/src/application/subsystem_catalog.cpp
namespace ork {
// Catalog subsystem factory function
// Implements graceful shutdown with operation drain
subsystem_ptr_t createCatalogSubsystem();
} // namespace orkGraceful Drain Pattern: Unlike GPU/Audio (simple RAII cleanup), the Catalog subsystem has in-flight async operations (downloads, fetches) that must complete before shutdown. It implements:
requestShutdown()- Signals all operations to stop, sets_shutdown_requestedflagdrainPendingOperations()- Waits for_inflight_requestscounter to reach zero- All blocking loops check
_shutdown_requestedfor early exit
See apphfsm.md section "Subsystem Graceful Shutdown Pattern" for details.
// server_app.cpp - Using ork.core only
#include <ork/application/application.h>
#include <ork/application/subsystem.h>
// Network-specific implementation (pimpl)
struct NetworkSubsystemImpl {
socket_ptr_t _socket;
};
// Network subsystem factory function
subsystem_ptr_t createNetworkSubsystem() {
auto subsystem = std::make_shared<ork::Subsystem>("network");
// Store impl using svar64_t (pimpl pattern)
auto impl = new NetworkSubsystemImpl();
subsystem->_impl.set<NetworkSubsystemImpl*>(impl);
// Build FSM states
subsystem->_data = std::make_shared<fsm::FsmData>();
subsystem->_state_uninitialized = subsystem->_data->createState(nullptr, "UNINITIALIZED");
subsystem->_state_initializing = subsystem->_data->createState(nullptr, "INITIALIZING");
subsystem->_state_ready = subsystem->_data->createState(nullptr, "READY");
subsystem->_state_shutting_down = subsystem->_data->createState(nullptr, "SHUTTING_DOWN");
subsystem->_state_terminated = subsystem->_data->createState(nullptr, "TERMINATED");
// Setup transitions
subsystem->_data->addTransition(subsystem->_state_uninitialized, "START", subsystem->_state_initializing);
subsystem->_data->addTransition(subsystem->_state_initializing, "INIT_DONE", subsystem->_state_ready);
subsystem->_data->addTransition(subsystem->_state_ready, "SHUTDOWN", subsystem->_state_shutting_down);
subsystem->_data->addTransition(subsystem->_state_shutting_down, "CLEANUP_DONE", subsystem->_state_terminated);
// Initialize callback
subsystem->_state_initializing->_onenter = [impl](fsm::fsminstance_ptr_t inst) {
// Initialize network sockets
impl->_socket = createServerSocket(8080);
inst->sendEvent("INIT_DONE");
};
// Shutdown callback
subsystem->_state_terminated->_onenter = [impl](fsm::fsminstance_ptr_t inst) {
delete impl;
};
// Create FSM instance
subsystem->_instance = fsm::FsmInstance::create(subsystem->_data);
subsystem->_instance->changeState(subsystem->_state_uninitialized);
return subsystem;
}
int main(int argc, char** argv) {
// Create core application (NO lev2 dependency)
auto initdata = std::make_shared<ork::AppInitData>(argc, argv);
auto app = ork::Application::create(initdata);
// Register network subsystem (static, no dependencies)
auto network = createNetworkSubsystem();
app->registerSubsystem("network", network, true);
// Run server loop (no rendering, just queue processing)
app->mainThreadLoop();
return 0;
}// gui_app.cpp - Using ork.lev2
#include <ork/lev2/ezapp.h>
#include <ork/ecs/subsystem.h>
class MyGuiApp {
public:
void run() {
// Create lev2 application (extends core Application)
auto initdata = std::make_shared<ork::AppInitData>(argc, argv);
auto app = lev2::OrkEzApp::create(initdata);
// Register static subsystems (GPU, Audio registered by EzApp automatically)
// ... GPU and Audio are built-in to EzApp ...
// User can still add custom subsystems (no dependencies)
auto network = std::make_shared<NetworkSubsystem>();
network->initialize();
app->registerSubsystem("network", network, true);
// Run GUI loop (includes rendering)
app->mainThreadLoop();
}
};// Level management (same pattern for server or GUI app)
void loadLevel(ork::application_ptr_t app, const std::string& level_name) {
std::string ecs_name = "ecs_" + level_name;
// Create ECS subsystem
auto ecs = ork::ecs::createEcsSubsystem();
// Set up dependencies via _dependencies map
// ECS depends on GPU if using lev2, or nothing if core-only
auto gpu_subsystem = app->findSubsystem("gpu");
if (gpu_subsystem) {
ecs->_dependencies["gpu"_crcu] = gpu_subsystem;
}
// Register dynamically (order is IMPLICIT from dependencies)
app->registerSubsystem(ecs_name, ecs, false); // is_static = false
}
void unloadLevel(ork::application_ptr_t app, const std::string& level_name) {
app->unregisterSubsystem("ecs_" + level_name);
}- Create
ork.core/inc/ork/application/application.h - Implement
Applicationbase class with FSM - Build core states (INIT, RUNNING, SHUTDOWN)
- Implement subsystem registration (from apphfsm.md)
- Implement OPQ queue management
- Implement UPDATE thread management
- Add virtual hooks for extension
- Write unit tests (server-only app)
- Create
ork.core/inc/ork/application/subsystem.h - Implement
Subsystembase class - Implement
SubsystemRegistrationstruct - Test with example network subsystem
- Add Python bindings for core Application
- Change
OrkEzAppto inherit fromork::Application - Override
buildApplicationFsm()to add GPU/Audio states - Override lifecycle hooks (onAppInitExtension, etc.)
- Move GPU-specific code to GpuSubsystem
- Move Audio-specific code to AudioSubsystem
- Test that existing lev2 apps still work
- Ensure backward compatibility with current callback API
- Update
apphfsm.mdto reflect core/lev2 split - Document core Application API
- Document extension pattern for custom app types
- Add server-side application examples
- Update application_framework_tdd.md
- Test server-only applications
- Test GUI applications
- Test dynamic subsystem registration
- Test multiple ECS instances
- Validate all subsystem coordination
- Performance benchmarking
Total Estimated Time: 3-4 weeks
✅ Standard lifecycle framework without graphics bloat ✅ FSM-based deterministic shutdown ✅ Subsystem coordination (network, database, etc.) ✅ OPQ queue management built-in ✅ Thread coordination (MAIN + UPDATE) ✅ No dependency on OpenGL/Vulkan/GLFW/Audio libraries
✅ All core benefits PLUS GPU/Audio/UI ✅ Clean inheritance hierarchy ✅ Easy to extend with custom subsystems ✅ Backward compatible with existing EzApp API ✅ Dynamic subsystem lifecycle (ECS per level)
✅ Clear separation of concerns ✅ Core has no lev2 dependencies (no circular deps) ✅ Easy to test (core can be tested independently) ✅ Extensible via virtual hooks (template method pattern) ✅ Follows Orkid patterns (factories, shared_ptr, FSM)
Existing OrkEzApp code continues to work:
# Existing Python app - NO CHANGES NEEDED
class MyApp(ComponentizedApplication):
def _onGpuInit(self, ctx):
# Still works!
passThe only change is internal - EzApp now inherits from Application.
New server apps can opt-in to core-only:
// NEW: Server app using core only
auto app = ork::Application::create(initdata);
app->mainThreadLoop(); // No graphics, just queues-
Should core Application support "headless GPU"?
- Proposal: No, keep core truly graphics-free. Use lev2 with offscreen rendering.
-
Should FREERUN/LOCKSTEP modes be in core or lev2?
- Proposal: Lev2 (rendering-specific). Core just has RUNNING state.
-
Should core have a "ConsoleApp" class between Application and EzApp?
- Proposal: Phase 2 feature - Application is enough for MVP.
-
How to handle Python bindings split?
- Proposal:
orkengine.core.Application- core bindingsorkengine.lev2.OrkEzApp(extends Application) - lev2 bindings
- Proposal:
This layered architecture provides:
- Core Application (ork.core) - FSM lifecycle for all apps
- EzApp (ork.lev2) - Extends with GPU/Audio/UI
- Clean separation - No circular dependencies
- Backward compatible - Existing code continues to work
- Server-side ready - ork.core can be used standalone
Next Step: Review this design, iterate, then implement Phase 1 (Core Application class).
Status: Design Phase - Core Layering Proposal Last Updated: 2026-01-12