diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..25bd570 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,508 @@ +# Trueno Engine — Architecture Overview + +**Version:** 0.6 +**Language:** Jai +**Target platforms:** macOS (native), Linux (native), WebAssembly (via Emscripten) + +--- + +## High-Level Structure + +Trueno is a purpose-built 3D game engine for a beach volleyball game. The codebase is split into +three clear layers: + +``` +┌─────────────────────────────────────────────────┐ +│ game/ │ Game logic (volleyball rules, players, state) +├─────────────────────────────────────────────────┤ +│ src/ │ Engine systems +├─────────────────────────────────────────────────┤ +│ sokol / stb_image / GetRect / Jaison / ... │ Third-party modules/ +└─────────────────────────────────────────────────┘ +``` + +The engine layer (`src/`) owns the main loop, rendering, audio, asset loading, and editor. The +game layer (`game/`) only implements game-specific logic and calls into the engine. There is no +formal interface between the two — they share a single Jai compilation unit — but the separation +is clean in practice. + +--- + +## Entry Points and the Frame Loop + +``` +sokol_app + └─ init() -- one-shot startup + └─ frame() -- called every display frame + └─ event() -- input / window events + └─ cleanup() -- shutdown +``` + +`src/platform_specific/` contains two thin files: `main_native.jai` (desktop) and `main_web.jai` +(WASM). Both call into `common.jai` which sets up sokol and then calls the engine's `init()`, +`frame()`, `event()`, `cleanup()` in `src/main.jai`. + +### Deferred Initialization + +Engine startup is split into three phases to handle async asset loading: + +``` +frame() tick 1..N + │ + ├─ asset_manager_tick() ← drives sokol_fetch async I/O + │ + ├─ mandatory_loads_done()? ──no─→ return (blocks entire frame) + │ yes + ├─ init_after_mandatory() ← (currently empty, reserved) + │ + ├─ show_loading_screen()? ──yes→ return (shows loading UI) + │ no + └─ init_after_core() ← init UI, audio, game, pipelines +``` + +The two boolean flags (`init_after_mandatory_done`, `init_after_core_done`) ensure each phase +runs exactly once after its prerequisite packs have loaded. + +### Fixed Timestep Game Loop + +```jai +delta_time_accumulator += delta_time; +while delta_time_accumulator > (1.0/60.0) { + game_tick(1.0/60.0); + delta_time_accumulator -= (1.0/60.0); +} +``` + +Game logic runs at a fixed 60 Hz regardless of render frame rate. Rendering always runs at the +display's native rate. The editor bypasses the accumulator entirely — when `in_editor_view` is +true, no game ticks run. + +--- + +## Directory Map + +``` +beachgame/ +├── first.jai Build metaprogram (compile-time build script) +├── build.sh / build_web.sh Shell wrappers around the Jai compiler +│ +├── src/ +│ ├── main.jai Engine core: init, frame loop, window info +│ ├── events.jai Sokol event dispatch → Input module +│ ├── load.jai Asset loading flags, init state +│ ├── time.jai get_time() / get_apollo_time() abstraction +│ ├── trile.jai Trile/Trixel/Material types, JSON serialization +│ ├── world.jai World/chunk data model, binary serialization +│ ├── ray.jai Ray-casting helpers +│ ├── utils.jai sign(), mix() math helpers +│ ├── profiling.jai Frame-level profiling points +│ ├── buffers.jai +│ ├── shapes.jai +│ │ +│ ├── assets/ +│ │ ├── asset_manager.jai Async fetch queue, pack/world/RDM loading +│ │ ├── loaders.jai PNG / WAV / string loaders from raw memory +│ │ └── rdm_loader.jai RDM chunk streaming enqueue/cancel +│ │ +│ ├── audio/ +│ │ ├── audio.jai Audio init/cleanup, Audio_Data type +│ │ ├── backend.jai Sokol audio stream callback bridge +│ │ ├── load.jai WAV loading helpers +│ │ └── mixer.jai Software float32 mixer (buses, tasks, mutex) +│ │ +│ ├── rendering/ +│ │ ├── rendering.jai Module root, window-resize handler +│ │ ├── core.jai init_rendering(), render() entry point +│ │ ├── tasks.jai Rendering_Task types and task→command conversion +│ │ ├── backend.jai Command bucket types, process_command_buckets() +│ │ ├── backend_sokol.jai All sokol draw call implementations +│ │ ├── backend_sokol_helpers.jai +│ │ ├── pipelines.jai Pipeline creation, all render target images +│ │ ├── camera.jai Camera struct, perspective/lookat math +│ │ ├── meshgen.jai Quad mesh generation from trile voxel data +│ │ ├── animation.jai Sprite animation player, Aseprite JSON structs +│ │ ├── helpers.jai create_world_rendering_tasks(), uniform helpers +│ │ ├── arbtri.jai Immediate-mode triangle batch renderer (UI) +│ │ ├── post_processing.jai Post-process config, LUT management +│ │ ├── sky.jai Sky rendering helpers +│ │ └── ssao.jai SSAO kernel/noise generation +│ │ +│ ├── editor/ +│ │ ├── editor.jai Editor root: three views, F3 toggle +│ │ ├── console.jai In-game console (F1), command dispatch +│ │ ├── level_editor.jai Level editing camera and trile placement tools +│ │ ├── trile_editor.jai Voxel painting editor +│ │ ├── tacoma.jai Path tracer (Tacoma) integration +│ │ ├── no_tacoma.jai Stub when Tacoma is unavailable +│ │ ├── rdm_disk.jai RDM bake result disk I/O +│ │ ├── picker.jai Mouse picker +│ │ ├── iprof.jai Iprof profiler integration +│ │ └── textureDebugger.jai Texture visualization tool +│ │ +│ ├── input/ +│ │ └── hotkeys.jai +│ │ +│ ├── ui/ +│ │ ├── ui.jai UI backend: GetRect integration, font, triangles +│ │ ├── component_themes.jai Custom UI themes +│ │ └── autoedit.jai Reflection-based automatic struct editors +│ │ +│ ├── pseudophysics/ +│ │ ├── core.jai +│ │ └── colliders.jai Rect-circle collision +│ │ +│ ├── platform_specific/ +│ │ ├── common.jai Sokol imports, sapp_run, context/temp storage +│ │ ├── main_native.jai Desktop entry point +│ │ ├── main_web.jai WASM entry point +│ │ ├── main.c C glue for Emscripten link +│ │ ├── runtime.js JS runtime helpers +│ │ └── shell.html WASM HTML shell +│ │ +│ ├── meta/ +│ │ ├── meta.jai Metaprogram: message handler entry point +│ │ ├── pack.jai Asset pack creation at build time +│ │ ├── shaderload.jai Adds compiled shaders to workspace +│ │ ├── lint.jai Compile-time snake_case linter +│ │ ├── console_commands.jai @Command auto-registration code generator +│ │ ├── ascii.jai ASCII art logo +│ │ └── hacks.jai Metaprogram workarounds +│ │ +│ └── shaders/ +│ ├── *.glsl GLSL shader sources (~12 shaders) +│ ├── jai/*.jai Compiled shader descriptors (sokol-shdc output) +│ └── compile_shaders*.sh +│ +├── game/ +│ ├── game.jai Game init, fixed-step tick, draw +│ ├── player.jai Player struct and input handling +│ └── state.jai Game state machine and scoring +│ +├── modules/ Third-party / custom Jai modules +│ ├── sokol-jai/ Jai bindings for sokol (gfx, app, audio, fetch) +│ ├── stb_image/ stb_image bindings +│ ├── Jaison/ JSON parsing/serialization +│ ├── Simple_Package_Reader/ .pack file reader +│ ├── Input/ Input module with sokol bindings +│ ├── Tacoma/ Path tracer integration +│ └── Walloc.jai WASM allocator +│ +├── resources/ Engine-level assets (fonts, textures, audio) +├── game/resources/ Game-level assets (worlds, sprites, audio) +└── packs/ Compiled .pack files (build output) +``` + +--- + +## Core Systems + +### Asset Manager + +All file I/O flows through a single sequential fetch queue backed by `sokol_fetch`. Only one file +is ever in-flight at a time. + +``` +load_pack() / load_world() + │ + ▼ + fetch_queue [..]Fetch_Request + │ + ▼ (asset_manager_tick, once per frame) + sfetch_send() ──async──► fetch_callback() + │ + resolve by Fetch_Type + ┌─────────┬──────────┬────────────┐ + PACK WORLD RDM_ATLAS RDM_LOOKUP + │ │ │ │ + Loaded_Pack World sg_make_image sg_make_image + in loadedPacks in current_world in chunk +``` + +Static pre-allocated I/O buffers are used for each fetch type (200 MB for packs/worlds, smaller +buffers for RDM data) so no runtime heap allocation occurs during file loading. + +**Blocking modes:** +- `should_block_engine` — halt entire frame (used for the boot pack; engine cannot run without it) +- `should_block` — show loading screen (used for core/game packs) + +### Rendering Pipeline + +The rendering system uses a three-stage data flow: + +``` +Game/Editor code + │ add_rendering_task() + ▼ +[Rendering_Task list] (per-frame, temp allocated) + │ tasks_to_commands() + ▼ +[Command Buckets] setup / shadow / reflection / gbuffer / main / ui + │ backend_process_command_buckets() + ▼ +Sokol GFX draw calls +``` + +**Render passes per frame (in order):** + +| Pass | Output | Notes | +|--------------|--------------------------------|----------------------------------------| +| Setup | GPU position/uniform buffers | Uploads trile instance stream data | +| Shadow | 1000×1000 depth texture | Orthographic sun directional light; PCF soft edges (Gaussian-weighted sampling) | +| Reflection | Half-res RGBA32F texture | Planar water reflection: camera Y-flipped around water plane; handles dynamic objects that RDMs cannot | +| G-Buffer | Position + Normal RGBA16F | Used by SSAO and deferred effects | +| SSAO | Single-channel AO texture | Random sample kernel, 64 samples | +| SSAO Blur | Blurred AO texture | Separable blur | +| Main | RGBA32F HDR target | Forward render: sun direct + RDM indirect (specular + diffuse) + SSAO | +| Post chain | Bloom → DoF ping-pong | Between postprocess_a / postprocess_b | +| Final | Swapchain | Tonemap, LUT grade, vignette, grain | + +**Trile rendering** uses GPU instancing. Each trile type's mesh is generated once (quad mesher, +`src/rendering/meshgen.jai`) and uploaded as a static vertex buffer. Per-frame instance +positions are streamed via `sg_append_buffer` into a dynamic buffer, with one draw call per +trile type per pass. + +Mesh generation merges visible trixel faces into larger coplanar quads (greedy meshing), reducing +triangle count dramatically vs. rendering each trixel individually. The mesh triangles no longer +correspond 1:1 to trixels, so the shader resolves the trixel under a fragment by offsetting the +surface position slightly inward along the normal to land cleanly inside the trixel grid. + +**Trixel material encoding:** each trixel stores 4 bytes — 3 bytes of RGB albedo and 1 byte +packed as: roughness (3 bits → 8 levels), metallic (2 bits → 4 levels), light emission (2 bits → +4 levels), plus 1 reserved bit. All material values map to the [0 … 1] range in the shader. + +**RDM (Rhombic Dodecahedron Mapping)** is the pre-baked indirect lighting system for static +trile geometry. Each chunk optionally carries an `rdm_atlas` (RGBA32F 4096×4096) and an +`rdm_lookup` (RGBA32F 512×512) GPU texture. The trile shader uses these for indirect diffuse +and specular, falling back to flat ambient when unavailable. + +The RDM concept: because all trile surface normals are axis-aligned, incoming light for a block +can be captured by storing exactly one hemisphere per face (6 total). Each hemisphere is encoded +with the **hemi-oct** projection (Cigolle et al. 2014) — the hemisphere is folded into a pyramid +and then flattened to a square. The six hemisphere squares are arranged in a 2×3 grid; together +they form a rhombic dodecahedron shape, giving the technique its name. + +Each hemisphere stores both **radiance** (RGBA32F) and **depth-to-first-hit** in the same +texel, encoded via a 4-channel HDR image (3 colour channels + 1 depth channel). The depth +is used at runtime for parallax-corrected sampling: a ray is stepped outward from the surface +point to find where it would intersect the geometry visible in the stored hemisphere, correcting +the angular discrepancy between the hemisphere's centre-of-face origin and the actual shading +point. + +**Multiple roughness levels:** the Tacoma path tracer pre-filters each RDM for 8 discrete +roughness values. Hemisphere resolution scales as 3·2^roughness × 2·2^roughness pixels, so +rougher (blurrier) RDMs are physically smaller. The maximum-roughness RDM (always baked) +doubles as the diffuse irradiance probe. Per-instance transform data passed alongside the GPU +instancing matrices encodes atlas UV offsets for roughness levels 1–7; the maximum-roughness +position is looked up separately from the lookup texture. + +**Specular evaluation:** the shader selects the RDM for the fragment's roughness, finds the +reflected view direction, steps the reflection vector to find the correct sampling direction via +stored depth, samples the hemisphere, then combines the result with a pre-computed BRDF +split-sum lookup (indexed by roughness and NoV) to form the full split-sum specular. + +**Diffuse evaluation:** the shader reads the maximum-roughness RDMs for the block and up to +three adjacent blocks (determined by which quadrant of the block face the fragment lies in), +samples each in the surface normal direction, and bilinearly blends the values. Missing +neighbours fall back to a static ambient constant. + +**RDM streaming:** RDM data is loaded per chunk on demand via `src/assets/rdm_loader.jai`. +Each chunk's atlas and lookup textures are allocated as `sg_image` resources and destroyed +when the chunk is unloaded (`src/editor/rdm_disk.jai` handles disk I/O in the editor). + +### World / Chunk System + +The world is a sparse hash map of chunks. Each chunk is a 32×32×32 block of the world at +integer trile-space coordinates. + +``` +World + └─ chunks: Table(Chunk_Key{x,y,z}, Chunk) + └─ groups: [..]Chunk_Trile_Group + ├─ trile_name: string + └─ instances: [..]Trile_Instance{x,y,z,orientation} +``` + +**Coordinate math:** +- World integer position → chunk key: floor division (`floor_div`) +- World integer position → local position (0..31): floor modulo (`floor_mod`) +- Both handle negative coordinates correctly (rounds toward -infinity) + +**Binary world format:** + +``` +[magic u32][version u16][name_len u16][name bytes] +[World_Config_Binary] +[num_chunks u32] +[chunk table: per chunk → chunk_x,y,z s32 × 3, data_offset u32, data_size u32] +[chunk data blobs] +``` + +### Lighting System + +Trueno uses a **hybrid lighting model**: real-time direct light from the sun combined with +pre-baked indirect and emissive light stored in per-chunk RDM textures. + +#### Direct lighting (sun) + +A single directional light ("sun") is evaluated per-fragment at runtime. Visibility is determined +with a shadow map rendered each frame from an orthographic projection centred on the camera +target. Percentage-closer filtering (PCF) with Gaussian-weighted sampling produces soft shadow +edges. Surfaces outside the shadow map area are assumed unshadowed (no cascades currently). + +#### Indirect lighting (RDM) + +The Rhombic Dodecahedron Mapping technique (described in the Rendering Pipeline section above) +provides pre-baked indirect and emissive light for static trile geometry. The bake captures both +diffuse and specular (including sharp mirror-like reflections) from the path tracer. + +**Emissive voxels:** materials with non-zero emission contribute to indirect lighting through the +RDM bake rather than being evaluated as real-time lights. This means dynamic objects only +receive sun light; they do not receive indirect light from emissive triles. + +**Split-sum specular:** the specular term combines the pre-filtered RDM sample with a +pre-integrated BRDF lookup texture (indexed by roughness and the angle between view direction +and surface normal), following the split-sum approximation. + +#### Water reflections (planar) + +The ocean/water surface uses planar reflections: the entire scene is rendered a second time +with the camera flipped across the water plane into a half-resolution RGBA32F target. This +handles dynamic objects (players, ball) that do not appear in the static RDMs, and produces +sharp mirror-like reflections correct for large flat surfaces. Post-processing can add ripple +distortion to the reflection texture. + +#### SSAO + +Screen-space ambient occlusion darkens cavities and contact areas. A geometry pass outputs +world-space position and normal (RGBA16F G-buffer), then the SSAO pass samples 64 random +offsets to estimate local occlusion. The result is blurred with a separable filter and applied to +the ambient/diffuse contribution. + +#### Pre-bake pipeline + +RDM data is generated offline by **Tacoma** (`modules/Tacoma/`, integrated via +`src/editor/tacoma.jai`). Tacoma is a GPU path tracer (C++/Vulkan) that: + +1. Builds a BLAS per unique trile type and assembles a TLAS with one instance per trile + occurrence in the scene. +2. For each block, detects which of the 8 roughness levels are present in its trixels and + renders one RDM per level. The maximum-roughness RDM is always rendered (needed for + diffuse). +3. Saves per-RDM HDR images (3 colour + 1 depth channel) and packs them into a single atlas + with a companion lookup file. + +The editor (`Level Studio`) triggers this bake; results are saved to disk via `src/editor/rdm_disk.jai` +and loaded at runtime via the chunk streaming system. + +### Audio Mixer + +The mixer runs on a background audio thread (native) or on the main thread (WASM). The sokol +audio callback calls `mixer_get_samples()` which iterates all active `Mixer_Play_Task` entries, +accumulates float samples into the output buffer, and removes finished tasks. + +``` +Mixer + ├─ config: Mixer_Config (per-bus volume, master volume) + ├─ tasks: [..]Mixer_Play_Task + │ └─ audio: *Audio_Data (pointer into pack memory) + │ bus: MUSIC | SOUND_EFFECT | DIALOGUE + │ mode: ONESHOT | REPEAT + │ curSample: s64 + └─ mutex (native only; no-op on WASM) +``` + +Thread safety: `mixer_add_task` and `mixer_get_samples` both acquire the mutex. On WASM, sokol +audio is single-threaded so the mutex is compiled out. + +### UI System + +The UI is built on top of `GetRect_LeftHanded` (immediate-mode retained UI) with a custom +immediate-mode triangle renderer (`arbtri`) as its drawing backend. All UI geometry is batched +into a single vertex buffer and drawn in the `ui` command bucket at the end of each frame. + +A viewport-relative unit system (`vw`/`vh`, each = 1/100 of window dimension) is used throughout +for responsive sizing. + +`autoedit.jai` provides reflection-based automatic struct editors: struct fields annotated with +`@Color`, `@Slider,min,max,step` etc. automatically get UI widgets generated at compile time via +Jai's type info system. + +### Editor + +The in-game editor (F3) has three views: + +- **Trile Studio** — voxel painting on a 16×16×16 grid with material selection +- **Level Studio** — orbiting camera, ray-based trile placement, Y-layer selection, optional + path-traced RDM bake via Tacoma integration +- **Material Studio** — lighting and world config parameters + +The developer console (F1) dispatches commands registered at compile time. Functions annotated +`@Command` are processed by the metaprogram during compilation: argument-parsing front-ends +(`%__command_front`) are auto-generated and registered in `console_command_names` / +`console_command_procs` arrays — zero runtime overhead. + +--- + +## Compile-Time Metaprogramming + +`first.jai` is the build script, executed by the Jai compiler as a metaprogram. It: + +1. Invokes `sokol-shdc` as a subprocess to compile GLSL shaders to backend-specific bytecode + and generate Jai descriptor structs +2. Packs all files under `resources/` and `game/resources/` (excluding `.aseprite` and `/worlds/`) + into binary `.pack` files +3. Adds the compiled shader descriptors to the compilation workspace +4. Runs the custom snake_case linter over all function declarations +5. Auto-generates `@Command` wrapper functions for the developer console + +This means asset pipeline and code generation happen as part of compilation, with no separate +build step needed. + +--- + +## Memory Model + +| Lifetime | Mechanism | +|-----------------|-----------------------------------------------------| +| Per-frame temp | `reset_temporary_storage()` at end of each frame | +| Mesh generation | `Pool.Pool` reset after each trile mesh is uploaded | +| World lifetime | `Pool.Pool` per `Current_World`, reset on unload | +| Asset loading | Static pre-allocated I/O buffers (never heap) | +| GPU resources | Explicit `sg_destroy_image/buffer` on replacement | +| Leak detection | `MEM_DEBUG :: true` enables Jai's memory debugger | + +--- + +## Platform Abstraction + +Platform differences are handled with `#if OS == .WASM / .MACOS / .LINUX` throughout: + +| Feature | Native | WASM | +|---------------------|----------------------|--------------------------| +| Audio thread mutex | Thread.Mutex | no-op | +| File I/O (editor) | File module | disabled | +| UV origin | Metal: flipped | WebGL: normal | +| Allocator | default Jai heap | Walloc | +| Profiler | Iprof available | disabled | +| Stack size | default | 1 GB (emcc flag) | +| WASM features | — | bulk-memory enabled | + +--- + +## Third-Party Dependencies + +| Library | Role | +|--------------------------|----------------------------------------------| +| sokol_gfx | Cross-platform graphics (Metal/GL/WebGL) | +| sokol_app | Window, input, event loop | +| sokol_audio | PCM audio stream callback | +| sokol_fetch | Async file / HTTP I/O | +| sokol_time | High-resolution timer | +| sokol_gl / sokol_fontstash | Immediate-mode lines, font rendering | +| stb_image | PNG decode | +| Jaison | JSON parse/serialize | +| GetRect_LeftHanded | Immediate-mode retained UI layout | +| Simple_Package_Reader | Binary .pack file reader | +| Walloc | WASM-compatible allocator | +| Tacoma | GPU path tracer (C++/Vulkan) for offline RDM baking; integrated via editor | +| Iprof | Optional frame profiler | diff --git a/docs/CODE_REVIEW.md b/docs/CODE_REVIEW.md new file mode 100644 index 0000000..e46a526 --- /dev/null +++ b/docs/CODE_REVIEW.md @@ -0,0 +1,264 @@ +# Trueno Engine — Code Review + +Overall this is a solid, well-structured engine. The architecture is clean, the compile-time +metaprogramming is used thoughtfully, and the explicit resource lifetime management is a real +strength. Below are the issues found, ordered from most critical downward. + +--- + +## 1. Real Bugs + +### WAV loader discards right channel but reports stereo +`src/assets/loaders.jai:44-49` + +```jai +for sample, i: audio_samples { + if i % 2 == 0 { // only keeps L channel + array_add(*audio.data, cast(float)sample / 32768.0); + } +} +audio.channels = format.nChannels; // but still reports 2 +``` + +The loader keeps every other sample (L only for stereo PCM) but stores the original `nChannels` +count. The mixer then uses `source_channels` to index into this data, reading out-of-range +positions for the right channel. Fix: either (a) fully decode both channels, or (b) store +`channels = 1` when you've downmixed to mono. The channel count and data layout are currently +inconsistent. + +### `clear_world` leaks RDM GPU textures +`src/world.jai:148-158` + +`clear_world()` frees instances/groups but never calls `sg_destroy_image` for chunks with +`rdm_valid == true`. `unload_current_world()` has the correct GPU cleanup loop — `clear_world` +needs the same treatment, or it should delegate to `unload_current_world` and re-init. + +--- + +## 2. Memory / Resource Leaks + +### `free_resources_from_pack` is empty +`src/assets/asset_manager.jai:279-281` + +```jai +free_resources_from_pack :: (pack: *Loaded_Pack) { + // empty +} +``` + +`add_resources_from_pack` uploads GPU textures (`sg_make_image`), copies audio data into heap +arrays, and allocates animation frame arrays. None of this is ever freed. If packs are ever +hot-reloaded or unloaded, every `sg_image`, every `Audio_Data.data` array, and every +`Animation.frames` array leaks. + +### `to_c_string` in `asset_manager_tick` leaks per fetch +`src/assets/asset_manager.jai:325-329` + +```jai +sfetch_send(*(sfetch_request_t.{ + path = to_c_string(req.path), // heap allocation, never freed + ... +})); +``` + +`to_c_string` allocates from the heap allocator. The pointer is discarded after `sfetch_send`. +One small leak per asset load. Store the result, pass it, then free it after the call (or +allocate with `temp` if sokol copies the path internally). + +### `fetch_callback` for PACK uses `NewArray` whose memory is never freed +`src/assets/asset_manager.jai:76-77` + +```jai +mem := NewArray(res.data.size.(s64), u8, false); +memcpy(mem.data, res.data.ptr, res.data.size.(s64)); +``` + +This heap-allocates the entire pack contents. With `free_resources_from_pack` being empty, this +memory is never freed. + +--- + +## 3. Crash on Corrupt Data + +### `read_value`/`read_string` use `assert()` for file bounds checks +`src/world.jai:240-250` + +```jai +read_value :: (data: []u8, cursor: *s64, $T: Type) -> T { + assert(cursor.* + size_of(T) <= data.count, "read_value: out of bounds"); + ... +} +``` + +`assert` crashes on a malformed or truncated world file. Since this data comes from disk it can +be corrupted or truncated. The `load_world_from_data` pattern of returning `(World, bool)` is +already in place — extend that into `read_value` returning `(T, bool)` so corrupt files are +rejected gracefully rather than crashing the process. + +--- + +## 4. Fixed Timestep "Spiral of Death" + +### No cap on `delta_time_accumulator` +`src/main.jai:198-201` + +```jai +while delta_time_accumulator > (1.0/60.0) { + game_tick(1.0/60.0); + delta_time_accumulator -= (1.0/60.0); +} +``` + +If a frame takes longer than one tick (debugging, loading spike, OS sleep), the accumulator grows +without bound. On the next frame many ticks run back-to-back, making that frame slow too — the +classic spiral. Add a cap before the while loop: + +```jai +delta_time_accumulator = min(delta_time_accumulator, 0.25); +``` + +There is also a **first-frame spike**: when loading finishes, `delta_time` includes all the time +spent loading (potentially seconds), blowing up the accumulator immediately. Reset +`last_frame_time` when `init_after_core_done` flips to true. + +--- + +## 5. Missing `sg_commit` During Loading + +### No frame commit when returning early +`src/main.jai:168-183` + +When `mandatory_loads_done()` returns false, `frame()` returns without calling `render()` (which +calls `sg_commit()`). Some backends accumulate state that must be flushed each frame. At minimum +do a clear pass + `sg_commit()` when returning early during loading, or call it unconditionally +at the end of `frame()`. + +--- + +## 6. World File Robustness + +### Instance count silently truncated to `u16` +`src/world.jai:311` + +```jai +count := cast(u16) group.instances.count; +``` + +If a chunk accumulates more than 65,535 instances of one trile type the count wraps silently on +save and the file is then unloadable. At minimum add an `assert(group.instances.count <= 0xFFFF)` +or widen to `u32`. + +### No file integrity check + +The binary format has magic + version but no CRC32 or checksum. A truncated or bit-flipped file +passes all header checks and then hits the assert crash. Even a simple FNV-1a checksum appended +at the end would let you detect corruption cleanly and emit a helpful error. + +### Chunk data offsets are `u32` + +Large worlds (>4 GB total chunk data) would overflow `running_offset`. Not a present concern but +worth noting if worlds grow significantly. + +--- + +## 7. Audio Mixer Output Clipping + +### No output saturation +`src/audio/mixer.jai:126` + +```jai +buffer[out_index] += sample * vol; +``` + +Multiple loud tracks sum without clamping. Sokol audio expects `[-1.0, 1.0]` float PCM. Values +outside that range clip hard on most backends or cause driver-level distortion. After the mixing +loop, clamp each output sample: + +```jai +buffer[i] = clamp(buffer[i], -1.0, 1.0); +``` + +--- + +## 8. Queue Performance + +### O(n) front removal in `asset_manager_tick` +`src/assets/asset_manager.jai:316-319` + +```jai +for i: 0..g_asset_manager.fetch_queue.count - 2 { + g_asset_manager.fetch_queue[i] = g_asset_manager.fetch_queue[i + 1]; +} +g_asset_manager.fetch_queue.count -= 1; +``` + +This shifts the entire array on every dequeue. With only a handful of queued fetches at a time +this is fine in practice, but a simple head-index (`fetch_queue_head: int`) avoids the shift +entirely. + +--- + +## 9. `find_pack_by_name` Returns by Value + +`src/assets/asset_manager.jai:283-292` + +```jai +find_pack_by_name :: (name: string) -> (bool, Loaded_Pack) { ... } +``` + +`Loaded_Pack` contains `Table` structs. Copying them and then calling `table_find_pointer` into +the copy is technically safe (the backing heap arrays are shared), but semantically wrong — you +get a copy of the struct, not a stable pointer into `g_asset_manager.loadedPacks`. If the +`loadedPacks` array is ever reallocated (another pack added while the copy is held), things get +subtly wrong. Change the return type to `*Loaded_Pack` and return null on not-found. + +--- + +## 10. `load_string_from_pack` Returns a Raw View + +`src/assets/asset_manager.jai:404-419` + +The function even comments "you are circumventing the asset management system." The returned +`string.data` points directly into the pack's internal content buffer. If the pack is ever freed +or `loadedPacks` reallocated, this string dangles. Either document that callers must +`copy_string` the result, or return a heap copy directly from this function. + +--- + +## 11. Minor Items + +- **`get_window_info()` returns hardcoded 1920x1080** (`src/main.jai:63-70`). The actual window + size is read from `sapp_width()/sapp_height()` everywhere else. This function appears to be + dead code — worth deleting to avoid confusion. + +- **`print("Should show loading screen....\n")` on every loading frame** (`src/main.jai:176`). + This spams stdout at 60 fps for the entire loading phase. Remove or guard with a one-shot flag. + +- **RDM header doesn't validate `width × height` fits within the static buffer** + (`src/assets/asset_manager.jai:121`). A malformed file with huge dimension values could cause + the `sg_image_data` range to extend past `rdm_atlas_buf`. Add an explicit bounds check: + `if atlas_pixel_bytes > RDM_ATLAS_MAX_BYTES - header_size then { ...; return; }`. + +- **`World_Config_Binary` field set is not versioned independently.** Adding new fields to + `World_Config` without bumping `WORLD_VERSION` silently breaks existing saved worlds. Consider + a migration path or at minimum a compile-time size check. + +--- + +## Summary + +| Priority | Area | Issue | +|------------|------------|----------------------------------------------------------| +| Bug | Audio | WAV loader discards right channel but reports stereo | +| Bug | World | `clear_world` leaks RDM GPU textures | +| Leak | Assets | `free_resources_from_pack` is empty; nothing freed | +| Leak | Assets | `to_c_string` result never freed per fetch | +| Crash | World | `assert()` on corrupt file data instead of error return | +| Stability | Game loop | No cap on `delta_time_accumulator` (spiral of death) | +| Stability | Game loop | First post-loading frame has enormous delta spike | +| Correctness| Rendering | No `sg_commit` during loading frames | +| Robustness | World | `u16` instance count silently truncates on save | +| Robustness | World | No file checksum / integrity check | +| Audio | Mixer | No output clamping; overlapping tracks clip | +| Performance| Assets | O(n) queue front removal on every tick | +| Correctness| Assets | `find_pack_by_name` returns copy instead of pointer | diff --git a/docs/RhombicDodecahedronMapping.pdf b/docs/RhombicDodecahedronMapping.pdf new file mode 100644 index 0000000..575f89b Binary files /dev/null and b/docs/RhombicDodecahedronMapping.pdf differ diff --git a/first.jai b/first.jai index 3dd9997..26de0a1 100644 --- a/first.jai +++ b/first.jai @@ -3,233 +3,200 @@ Iprof :: #import "Iprof"(IMPORT_MODE = .METAPROGRAM); #import "File_Utilities"; -#run { - print("%\n", ascii_car); +Trueno_Build_Options :: struct { + wasm_build : bool; + release_build : bool; + tacoma_enabled : bool; + iprof_enabled : bool; +} - // Step 1. Compile shaders. - { - print("--- Shader Compile Step ---\n"); - process_result, output, error := run_command("bash", ifx OS == .MACOS then "./compile_shaders_mac.sh" else "./compile_shaders.sh", working_directory=tprint("%/src/shaders", #filepath)); - if process_result.exit_code != 0 { - log_error("Shader compilation failed."); - if output { - log_error(output,, logger = runtime_support_default_logger); - } - exit(1); - } - } +build_options_from_args :: (args: []string) -> Trueno_Build_Options { + opts : Trueno_Build_Options; - // Step 2. Create a pack file for all of the assets in ./resources. - create_pack(); - - opt := get_build_options(); - args := opt.compile_time_command_line; - doWasmBuild := false; - hasTacoma := false; - releaseBuild := false; - iprof := false; - uncap_frames := false; - for arg : args { - if arg == "wasm" then doWasmBuild = true; - if arg == "tacoma" then hasTacoma = true; - if arg == "release" then releaseBuild = true; - if arg == "iprof" then iprof = true; - if arg == "iprof" then iprof = true; - if arg == "uncap_frames" then uncap_frames = true; + if arg == { + case "wasm"; + opts.wasm_build = true; + case "tacoma"; + opts.tacoma_enabled = true; + case "release"; + opts.release_build = true; + case "iprof"; + opts.iprof_enabled = true; + } } - if doWasmBuild { + return opts; +} + +add_trueno_opts_to_compiler_strings :: (trueno_opts : Trueno_Build_Options, w: Workspace) { + #import "String"; + bool_to_string :: (b: bool) -> string { + if b then return tprint("true"); else return tprint("false"); + } + + trueno_opts_info := type_info(Trueno_Build_Options); + for trueno_opts_info.members { + optPtr := (cast(*bool)(*trueno_opts)) + it.offset_in_bytes; + name := tprint("%", it.name); + to_upper_in_place(name); + add_build_string(tprint("FLAG_% :: %;\n", name, bool_to_string(optPtr.*)), w); + } +} + +compile_shaders :: () { + process_result, output, error := run_command("bash", ifx OS == .MACOS then "./compile_shaders_mac.sh" else "./compile_shaders.sh", working_directory=tprint("%/src/shaders", #filepath)); + if process_result.exit_code != 0 { + log_error("Shader compilation failed."); + if output { + log_error(output,, logger = runtime_support_default_logger); + } + exit(1); + } +} + +wasm_copy_assets :: () { + if is_directory("./packs") { + run_command("cp", "-r", "./packs", "./dist/", working_directory=tprint("%", #filepath)); + } + + worlds_src := "./game/resources/worlds"; + if is_directory(worlds_src) { + make_directory_if_it_does_not_exist("./dist/game/resources", recursive = true); + run_command("cp", "-r", worlds_src, "./dist/game/resources/", working_directory=tprint("%", #filepath)); + } +} + +metaprogramming_loop :: (trueno_opts: Trueno_Build_Options, w: *Workspace) { + while true { + message := compiler_wait_for_message(); + // @ToDo: add iprof to this refactored metaprogram + // if trueno_opts.iprof_enabled then iprof_plugin.message(iprof_plugin, message) + custom_message_handler(message, w); + if message.kind == .COMPLETE then break; + } + compiler_end_intercept(w.*); +} + +native_build :: (opts: Build_Options, trueno_opts: Trueno_Build_Options) { + set_build_options_dc(.{do_output=false}); + + current_w := get_current_workspace(); + root_opts := get_build_options(); + w := compiler_create_workspace("Target"); + opts := get_build_options(w); + copy_commonly_propagated_fields(opts, *root_opts); + + opts.cpu_target = root_opts.cpu_target; + opts.os_target = root_opts.os_target; + opts.backend = ifx (trueno_opts.release_build || OS == .MACOS) then .LLVM else .X64; + opts.output_executable_name = root_opts.output_executable_name; + + set_build_options(opts, w); + iprof_plugin: *Iprof.My_Plugin; + + // profile := iprof; + + // if profile { + // iprof_plugin = cast(*Iprof.My_Plugin) Iprof.get_plugin(); + // iprof_plugin.workspace = w; + + // // Set options + // iprof_plugin.instrument_modules = true; + + // iprof_plugin.before_intercept(iprof_plugin, null); + // } + + compiler_begin_intercept(w); + + add_build_file("src/platform_specific/main_native.jai", w); + add_shaders_to_workspace(w); + + metaprogramming_loop(trueno_opts, *w); + + if trueno_opts.iprof_enabled { + iprof_plugin.finish(iprof_plugin); + iprof_plugin.shutdown(iprof_plugin); + } +} + +wasm_build :: (opts: Build_Options, trueno_opts: Trueno_Build_Options) { set_build_options_dc(.{do_output = false}); - make_directory_if_it_does_not_exist("dist", recursive = true); - { - // Copy compiled packs (produced by create_pack above). - if is_directory("./packs") { - run_command("cp", "-r", "./packs", "./dist/", working_directory=tprint("%", #filepath)); - } - - // Copy only the worlds subtree — not the whole game dir (avoids .git etc.). - worlds_src := "./game/resources/worlds"; - if is_directory(worlds_src) { - make_directory_if_it_does_not_exist("./dist/game/resources", recursive = true); - run_command("cp", "-r", worlds_src, "./dist/game/resources/", working_directory=tprint("%", #filepath)); - } - - - w := compiler_create_workspace("Wasm"); - - options := get_build_options(w); - copy_commonly_propagated_fields(get_build_options(), *options); - - options.output_type = .OBJECT_FILE; - options.backend = .LLVM; // WASM only works with the LLVM backend, obviously. - options.os_target = .WASM; - options.cpu_target = .CUSTOM; - options.emit_debug_info = .DWARF; - options.backtrace_on_crash = .OFF; // Runtime_Support_Crash_Handler doesn’t support WASM (yet?) - options.output_path = "dist/"; - options.output_executable_name = "main"; - options.llvm_options.target_system_features = "+bulk-memory"; // "This options is needed so that "memcpy" and "memset" are mapped to "memory.copy" and "memory.fill" instructions in WASM. - options.llvm_options.enable_split_modules = false; - options.llvm_options.function_sections = true; // To get around "LLVM ERROR: section already has a defining function: .text" - - import_paths: [..]string; - // Add our own modules folder first so that we can override modules with our own version, if necessary. - for options.import_path array_add(*import_paths, it); - options.import_path = import_paths; - - // This was compiled from https://github.com/wingo/walloc via "clang -Oz --target=wasm64 -nostdlib -c -o walloc.o walloc.c". - // We should probably port this allocator to Jai instead… - // -rluba, 2023-11-15 - walloc_object_file_path := "walloc.o"; - - STACK_SIZE :: 1024 * 1024 * 1024; - options.additional_linker_arguments = .["--stack-first", "-z", tprint("stack-size=%", STACK_SIZE), walloc_object_file_path]; - - set_build_options(options, w); - - // Replace the default allocator with Walloc (https://github.com/wingo/walloc). - remap_import(w, "*", "Default_Allocator", "Walloc"); - - compiler_begin_intercept(w); - - add_build_file("src/platform_specific/main_web.jai", w); - add_build_string("HAS_TACOMA :: false;", w); - add_build_string("HAS_IPROF :: false;", w); - add_build_string("UNCAPPED_FRAMES :: false;", w); - - if releaseBuild { - add_build_string("RELEASE_BUILD :: true;", w); - } else { - add_build_string("RELEASE_BUILD :: false;", w); - } - - if is_directory("./game") { - add_build_string("USE_SAMPLE_GAME :: false;", w); - } else { - add_build_string("USE_SAMPLE_GAME :: true;", w); - - } - - - add_shaders_to_workspace(w); - - while true { - message := compiler_wait_for_message(); - custom_message_handler(message, *w); - if message.kind == { - case .TYPECHECKED; - typechecked := cast(*Message_Typechecked) message; - for body: typechecked.procedure_bodies { - header := body.expression.header; - // You could replace individual procedure bodies here, if you needed to. - } - - case .COMPLETE; - break; - } - } - - compiler_end_intercept(w); - } - - { - args := string.[ - "emcc", - "src/platform_specific/main.c", "dist/main.o", "modules/sokol-jai/sokol/gfx/sokol_gfx_wasm_gl_release.a", "modules/sokol-jai/sokol/log/sokol_log_wasm_gl_release.a", "modules/sokol-jai/sokol/audio/sokol_audio_wasm_gl_release.a", "modules/sokol-jai/sokol/time/sokol_time_wasm_gl_release.a", "modules/sokol-jai/sokol/app/sokol_app_wasm_gl_release.a", "modules/sokol-jai/sokol/glue/sokol_glue_wasm_gl_release.a", "modules/sokol-jai/sokol/fetch/sokol_fetch_wasm_gl_release.a", "modules/sokol-jai/sokol/gl/sokol_gl_wasm_gl_release.a", "modules/sokol-jai/sokol/fontstash/sokol_fontstash_wasm_gl_release.a", - "modules/sokol-jai/sokol/stbi/stb_image.a", - "-o", "dist/index.html", - "-sSTACK_SIZE=10MB", - "-sALLOW_MEMORY_GROWTH", - "-sERROR_ON_UNDEFINED_SYMBOLS=1", "-sMEMORY64", "-sMAX_WEBGL_VERSION=2", - "--js-library=src/platform_specific/runtime.js", - "--shell-file=src/platform_specific/shell.html", - ]; - process_result, output, error := run_command(..args, capture_and_return_output = true); - if process_result.exit_code != 0 { - log_error("EMCC compilation failed."); - if error { - log_error(error,, logger = runtime_support_default_logger); - } - exit(1); - } - } - } else { - set_build_options_dc(.{do_output=false}); - - current_w := get_current_workspace(); - root_opts := get_build_options(); - w := compiler_create_workspace("Target"); - opts := get_build_options(w); - copy_commonly_propagated_fields(opts, *root_opts); - - opts.cpu_target = root_opts.cpu_target; - opts.os_target = root_opts.os_target; - opts.backend = ifx (releaseBuild || OS == .MACOS) then .LLVM else .X64; - opts.output_executable_name = root_opts.output_executable_name; - - set_build_options(opts, w); - iprof_plugin: *Iprof.My_Plugin; - - profile := iprof; - - if profile { - iprof_plugin = cast(*Iprof.My_Plugin) Iprof.get_plugin(); - iprof_plugin.workspace = w; - // Set options - iprof_plugin.instrument_modules = true; + wasm_copy_assets(); + w := compiler_create_workspace("Wasm"); - iprof_plugin.before_intercept(iprof_plugin, null); - } + options := get_build_options(w); + copy_commonly_propagated_fields(get_build_options(), *options); - compiler_begin_intercept(w); - if hasTacoma { - add_build_string("HAS_TACOMA :: true;", w); - } else { - add_build_string("HAS_TACOMA :: false;", w); - } + options.output_type = .OBJECT_FILE; + options.backend = .LLVM; + options.os_target = .WASM; + options.cpu_target = .CUSTOM; + options.emit_debug_info = .DWARF; + options.backtrace_on_crash = .OFF; + options.output_path = "dist/"; + options.output_executable_name = "main"; + options.llvm_options.target_system_features = "+bulk-memory"; + options.llvm_options.enable_split_modules = false; + options.llvm_options.function_sections = true; - if is_directory("./game") { - add_build_string("USE_SAMPLE_GAME :: false;", w); - } else { - add_build_string("USE_SAMPLE_GAME :: true;", w); - } + import_paths: [..]string; - if uncap_frames { - add_build_string("UNCAPPED_FRAMES :: true;", w); - } else { - add_build_string("UNCAPPED_FRAMES :: false;", w); - } + for options.import_path array_add(*import_paths, it); + options.import_path = import_paths; - if releaseBuild { - add_build_string("RELEASE_BUILD :: true;", w); - } else { - add_build_string("RELEASE_BUILD :: false;", w); - } + // This was compiled from https://github.com/wingo/walloc via "clang -Oz --target=wasm64 -nostdlib -c -o walloc.o walloc.c". + // We should probably port this allocator to Jai instead… + // -rluba, 2023-11-15 + walloc_object_file_path := "walloc.o"; + STACK_SIZE :: 1024 * 1024 * 1024; + options.additional_linker_arguments = .["--stack-first", "-z", tprint("stack-size=%", STACK_SIZE), walloc_object_file_path]; - if profile { - add_build_string("HAS_IPROF :: true;", w); - iprof_plugin.add_source(iprof_plugin); - } else { - add_build_string("HAS_IPROF :: false;", w); - } - add_build_file("src/platform_specific/main_native.jai", w); - add_shaders_to_workspace(w); + set_build_options(options, w); + remap_import(w, "*", "Default_Allocator", "Walloc"); - while true { - message := compiler_wait_for_message(); - if profile then iprof_plugin.message(iprof_plugin, message); - custom_message_handler(message, *w); - if message.kind == .COMPLETE then break; - } - compiler_end_intercept(w); - if profile { - iprof_plugin.finish(iprof_plugin); - iprof_plugin.shutdown(iprof_plugin); - } + compiler_begin_intercept(w); + + add_trueno_opts_to_compiler_strings(trueno_opts, w); + add_build_file("src/platform_specific/main_web.jai", w); + add_shaders_to_workspace(w); + + metaprogramming_loop(trueno_opts, *w); + + args := string.[ + "emcc", + "src/platform_specific/main.c", "dist/main.o", "modules/sokol-jai/sokol/gfx/sokol_gfx_wasm_gl_release.a", "modules/sokol-jai/sokol/log/sokol_log_wasm_gl_release.a", "modules/sokol-jai/sokol/audio/sokol_audio_wasm_gl_release.a", "modules/sokol-jai/sokol/time/sokol_time_wasm_gl_release.a", "modules/sokol-jai/sokol/app/sokol_app_wasm_gl_release.a", "modules/sokol-jai/sokol/glue/sokol_glue_wasm_gl_release.a", "modules/sokol-jai/sokol/fetch/sokol_fetch_wasm_gl_release.a", "modules/sokol-jai/sokol/gl/sokol_gl_wasm_gl_release.a", "modules/sokol-jai/sokol/fontstash/sokol_fontstash_wasm_gl_release.a", + "modules/sokol-jai/sokol/stbi/stb_image.a", + "-o", "dist/index.html", + "-sSTACK_SIZE=10MB", + "-sALLOW_MEMORY_GROWTH", + "-sERROR_ON_UNDEFINED_SYMBOLS=1", "-sMEMORY64", "-sMAX_WEBGL_VERSION=2", + "--js-library=src/platform_specific/runtime.js", + "--shell-file=src/platform_specific/shell.html", + ]; + process_result, output, error := run_command(..args, capture_and_return_output = true); + if process_result.exit_code != 0 { + log_error("EMCC compilation failed."); + if error { + log_error(error,, logger = runtime_support_default_logger); + } + exit(1); + } +} + +#run { + opt := get_build_options(); + trueno_opts := build_options_from_args(opt.compile_time_command_line); + compile_shaders(); + create_pack(); + + if trueno_opts.wasm_build { + wasm_build(opt, trueno_opts); + } else { + native_build(opt, trueno_opts); } } diff --git a/src/editor/editor.jai b/src/editor/editor.jai index 3fc34c7..db1370e 100644 --- a/src/editor/editor.jai +++ b/src/editor/editor.jai @@ -5,7 +5,7 @@ #load "trile_editor.jai"; #load "level_editor.jai"; } -#if HAS_TACOMA { #load "tacoma.jai"; } +#if FLAG_TACOMA_ENABLED { #load "tacoma.jai"; } #load "console.jai"; #load "textureDebugger.jai"; diff --git a/src/editor/level_editor.jai b/src/editor/level_editor.jai index 9bc8bba..19aee1c 100644 --- a/src/editor/level_editor.jai +++ b/src/editor/level_editor.jai @@ -175,7 +175,7 @@ draw_tacoma_tab :: (theme: *GR.Overall_Theme, total_r: GR.Rect) { curworld := get_current_world(); r := total_r; r.h = ui_h(3,0); - #if HAS_TACOMA { + #if FLAG_TACOMA_ENABLED { if GR.button(r, "Render with Tacoma", *theme.button_theme) { cam := get_level_editor_camera(); gen_reference(tacomaResolution, tacomaResolution, cam.position, cam.target, tacomaSamples, true, curworld.world); @@ -314,7 +314,7 @@ remove_trile :: (x: s32, y: s32, z: s32) { tick_level_editor :: () { - #if HAS_TACOMA { rdm_bake_tick(); } + #if FLAG_TACOMA_ENABLED { rdm_bake_tick(); } tick_level_editor_camera(); ray := get_mouse_ray(*get_level_editor_camera()); hit, point := ray_plane_collision_point(ray, xx editY, 20); diff --git a/src/main.jai b/src/main.jai index 1a9c16e..a479017 100644 --- a/src/main.jai +++ b/src/main.jai @@ -31,11 +31,7 @@ stbi :: #import "stb_image"; #load "audio/audio.jai"; #load "assets/asset_manager.jai"; -#if USE_SAMPLE_GAME { - #load "../sample_game/game.jai"; -} else { - #load "../game/game.jai"; -} +#load "../game/game.jai"; last_frame_time : float64; // timestamp of the last frame delta\ _time : float64; diff --git a/src/platform_specific/common.jai b/src/platform_specific/common.jai index 29342e8..16e131d 100644 --- a/src/platform_specific/common.jai +++ b/src/platform_specific/common.jai @@ -1,13 +1,13 @@ -#import,dir "../../modules/sokol-jai/sokol/app"(DEBUG = RELEASE_BUILD); -#import,dir "../../modules/sokol-jai/sokol/gfx"(DEBUG = RELEASE_BUILD); -#import,dir "../../modules/sokol-jai/sokol/gl"(DEBUG = RELEASE_BUILD); -#import,dir "../../modules/sokol-jai/sokol/glue"(DEBUG = RELEASE_BUILD); -#import,dir "../../modules/sokol-jai/sokol/shape"(DEBUG = RELEASE_BUILD); +#import,dir "../../modules/sokol-jai/sokol/app"(DEBUG = FLAG_RELEASE_BUILD); +#import,dir "../../modules/sokol-jai/sokol/gfx"(DEBUG = FLAG_RELEASE_BUILD); +#import,dir "../../modules/sokol-jai/sokol/gl"(DEBUG = FLAG_RELEASE_BUILD); +#import,dir "../../modules/sokol-jai/sokol/glue"(DEBUG = FLAG_RELEASE_BUILD); +#import,dir "../../modules/sokol-jai/sokol/shape"(DEBUG = FLAG_RELEASE_BUILD); #import,dir "../../modules/sokol-jai/sokol/fontstash"; -#import,dir "../../modules/sokol-jai/sokol/log"(DEBUG = RELEASE_BUILD); -#import,dir "../../modules/sokol-jai/sokol/time"(DEBUG = RELEASE_BUILD); -#import,dir "../../modules/sokol-jai/sokol/fetch"(DEBUG = RELEASE_BUILD); -#import,dir "../../modules/sokol-jai/sokol/audio"(DEBUG = RELEASE_BUILD); +#import,dir "../../modules/sokol-jai/sokol/log"(DEBUG = FLAG_RELEASE_BUILD); +#import,dir "../../modules/sokol-jai/sokol/time"(DEBUG = FLAG_RELEASE_BUILD); +#import,dir "../../modules/sokol-jai/sokol/fetch"(DEBUG = FLAG_RELEASE_BUILD); +#import,dir "../../modules/sokol-jai/sokol/audio"(DEBUG = FLAG_RELEASE_BUILD); #load "../main.jai";