Merge branch 'main' of git.ktj.st:katajisto/trueno
This commit is contained in:
commit
4697c016e1
508
docs/ARCHITECTURE.md
Normal file
508
docs/ARCHITECTURE.md
Normal file
@ -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 |
|
||||||
264
docs/CODE_REVIEW.md
Normal file
264
docs/CODE_REVIEW.md
Normal file
@ -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 |
|
||||||
BIN
docs/RhombicDodecahedronMapping.pdf
Normal file
BIN
docs/RhombicDodecahedronMapping.pdf
Normal file
Binary file not shown.
389
first.jai
389
first.jai
@ -3,233 +3,200 @@
|
|||||||
Iprof :: #import "Iprof"(IMPORT_MODE = .METAPROGRAM);
|
Iprof :: #import "Iprof"(IMPORT_MODE = .METAPROGRAM);
|
||||||
#import "File_Utilities";
|
#import "File_Utilities";
|
||||||
|
|
||||||
#run {
|
Trueno_Build_Options :: struct {
|
||||||
print("%\n", ascii_car);
|
wasm_build : bool;
|
||||||
|
release_build : bool;
|
||||||
|
tacoma_enabled : bool;
|
||||||
|
iprof_enabled : bool;
|
||||||
|
}
|
||||||
|
|
||||||
// Step 1. Compile shaders.
|
build_options_from_args :: (args: []string) -> Trueno_Build_Options {
|
||||||
{
|
opts : Trueno_Build_Options;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
for arg : args {
|
||||||
if arg == "wasm" then doWasmBuild = true;
|
if arg == {
|
||||||
if arg == "tacoma" then hasTacoma = true;
|
case "wasm";
|
||||||
if arg == "release" then releaseBuild = true;
|
opts.wasm_build = true;
|
||||||
if arg == "iprof" then iprof = true;
|
case "tacoma";
|
||||||
if arg == "iprof" then iprof = true;
|
opts.tacoma_enabled = true;
|
||||||
if arg == "uncap_frames" then uncap_frames = 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});
|
set_build_options_dc(.{do_output = false});
|
||||||
|
|
||||||
make_directory_if_it_does_not_exist("dist", recursive = true);
|
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
|
wasm_copy_assets();
|
||||||
iprof_plugin.instrument_modules = true;
|
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);
|
options.output_type = .OBJECT_FILE;
|
||||||
if hasTacoma {
|
options.backend = .LLVM;
|
||||||
add_build_string("HAS_TACOMA :: true;", w);
|
options.os_target = .WASM;
|
||||||
} else {
|
options.cpu_target = .CUSTOM;
|
||||||
add_build_string("HAS_TACOMA :: false;", w);
|
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") {
|
import_paths: [..]string;
|
||||||
add_build_string("USE_SAMPLE_GAME :: false;", w);
|
|
||||||
} else {
|
|
||||||
add_build_string("USE_SAMPLE_GAME :: true;", w);
|
|
||||||
}
|
|
||||||
|
|
||||||
if uncap_frames {
|
for options.import_path array_add(*import_paths, it);
|
||||||
add_build_string("UNCAPPED_FRAMES :: true;", w);
|
options.import_path = import_paths;
|
||||||
} else {
|
|
||||||
add_build_string("UNCAPPED_FRAMES :: false;", w);
|
|
||||||
}
|
|
||||||
|
|
||||||
if releaseBuild {
|
// This was compiled from https://github.com/wingo/walloc via "clang -Oz --target=wasm64 -nostdlib -c -o walloc.o walloc.c".
|
||||||
add_build_string("RELEASE_BUILD :: true;", w);
|
// We should probably port this allocator to Jai instead…
|
||||||
} else {
|
// -rluba, 2023-11-15
|
||||||
add_build_string("RELEASE_BUILD :: false;", w);
|
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 {
|
set_build_options(options, w);
|
||||||
add_build_string("HAS_IPROF :: true;", w);
|
remap_import(w, "*", "Default_Allocator", "Walloc");
|
||||||
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);
|
|
||||||
|
|
||||||
while true {
|
compiler_begin_intercept(w);
|
||||||
message := compiler_wait_for_message();
|
|
||||||
if profile then iprof_plugin.message(iprof_plugin, message);
|
add_trueno_opts_to_compiler_strings(trueno_opts, w);
|
||||||
custom_message_handler(message, *w);
|
add_build_file("src/platform_specific/main_web.jai", w);
|
||||||
if message.kind == .COMPLETE then break;
|
add_shaders_to_workspace(w);
|
||||||
}
|
|
||||||
compiler_end_intercept(w);
|
metaprogramming_loop(trueno_opts, *w);
|
||||||
if profile {
|
|
||||||
iprof_plugin.finish(iprof_plugin);
|
args := string.[
|
||||||
iprof_plugin.shutdown(iprof_plugin);
|
"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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
#load "trile_editor.jai";
|
#load "trile_editor.jai";
|
||||||
#load "level_editor.jai";
|
#load "level_editor.jai";
|
||||||
}
|
}
|
||||||
#if HAS_TACOMA { #load "tacoma.jai"; }
|
#if FLAG_TACOMA_ENABLED { #load "tacoma.jai"; }
|
||||||
#load "console.jai";
|
#load "console.jai";
|
||||||
#load "textureDebugger.jai";
|
#load "textureDebugger.jai";
|
||||||
|
|
||||||
|
|||||||
@ -203,7 +203,7 @@ draw_tacoma_tab :: (theme: *GR.Overall_Theme, total_r: GR.Rect) {
|
|||||||
curworld := get_current_world();
|
curworld := get_current_world();
|
||||||
r := total_r;
|
r := total_r;
|
||||||
r.h = ui_h(3,0);
|
r.h = ui_h(3,0);
|
||||||
#if HAS_TACOMA {
|
#if FLAG_TACOMA_ENABLED {
|
||||||
if GR.button(r, "Render with Tacoma", *theme.button_theme) {
|
if GR.button(r, "Render with Tacoma", *theme.button_theme) {
|
||||||
cam := get_level_editor_camera();
|
cam := get_level_editor_camera();
|
||||||
gen_reference(tacomaResolution, tacomaResolution, cam.position, cam.target, tacomaSamples, true, curworld.world);
|
gen_reference(tacomaResolution, tacomaResolution, cam.position, cam.target, tacomaSamples, true, curworld.world);
|
||||||
@ -452,7 +452,7 @@ remove_trile :: (x: s32, y: s32, z: s32) {
|
|||||||
|
|
||||||
|
|
||||||
tick_level_editor :: () {
|
tick_level_editor :: () {
|
||||||
#if HAS_TACOMA { rdm_bake_tick(); }
|
#if FLAG_TACOMA_ENABLED { rdm_bake_tick(); }
|
||||||
tick_level_editor_camera();
|
tick_level_editor_camera();
|
||||||
|
|
||||||
if !console_open_ignore_input {
|
if !console_open_ignore_input {
|
||||||
|
|||||||
@ -30,13 +30,8 @@ stbi :: #import "stb_image";
|
|||||||
#load "utils.jai";
|
#load "utils.jai";
|
||||||
#load "audio/audio.jai";
|
#load "audio/audio.jai";
|
||||||
#load "assets/asset_manager.jai";
|
#load "assets/asset_manager.jai";
|
||||||
#load "pack_hotreload.jai";
|
|
||||||
|
#load "../game/game.jai";
|
||||||
#if USE_SAMPLE_GAME {
|
|
||||||
#load "../sample_game/game.jai";
|
|
||||||
} else {
|
|
||||||
#load "../game/game.jai";
|
|
||||||
}
|
|
||||||
|
|
||||||
last_frame_time : float64; // timestamp of the last frame
|
last_frame_time : float64; // timestamp of the last frame
|
||||||
delta\ _time : float64;
|
delta\ _time : float64;
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
#import,dir "../../modules/sokol-jai/sokol/app"(DEBUG = RELEASE_BUILD);
|
#import,dir "../../modules/sokol-jai/sokol/app"(DEBUG = FLAG_RELEASE_BUILD);
|
||||||
#import,dir "../../modules/sokol-jai/sokol/gfx"(DEBUG = RELEASE_BUILD);
|
#import,dir "../../modules/sokol-jai/sokol/gfx"(DEBUG = FLAG_RELEASE_BUILD);
|
||||||
#import,dir "../../modules/sokol-jai/sokol/gl"(DEBUG = RELEASE_BUILD);
|
#import,dir "../../modules/sokol-jai/sokol/gl"(DEBUG = FLAG_RELEASE_BUILD);
|
||||||
#import,dir "../../modules/sokol-jai/sokol/glue"(DEBUG = RELEASE_BUILD);
|
#import,dir "../../modules/sokol-jai/sokol/glue"(DEBUG = FLAG_RELEASE_BUILD);
|
||||||
#import,dir "../../modules/sokol-jai/sokol/shape"(DEBUG = 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/fontstash";
|
||||||
#import,dir "../../modules/sokol-jai/sokol/log"(DEBUG = RELEASE_BUILD);
|
#import,dir "../../modules/sokol-jai/sokol/log"(DEBUG = FLAG_RELEASE_BUILD);
|
||||||
#import,dir "../../modules/sokol-jai/sokol/time"(DEBUG = RELEASE_BUILD);
|
#import,dir "../../modules/sokol-jai/sokol/time"(DEBUG = FLAG_RELEASE_BUILD);
|
||||||
#import,dir "../../modules/sokol-jai/sokol/fetch"(DEBUG = RELEASE_BUILD);
|
#import,dir "../../modules/sokol-jai/sokol/fetch"(DEBUG = FLAG_RELEASE_BUILD);
|
||||||
#import,dir "../../modules/sokol-jai/sokol/audio"(DEBUG = RELEASE_BUILD);
|
#import,dir "../../modules/sokol-jai/sokol/audio"(DEBUG = FLAG_RELEASE_BUILD);
|
||||||
|
|
||||||
#load "../main.jai";
|
#load "../main.jai";
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user