WIP: commit old refactor work from laptop
This commit is contained in:
parent
3a7f19c89d
commit
1de37e8719
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);
|
||||
#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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user