#run { #load "../meta/ascii.jai"; print("%\n", ascii_tacoma); } Tacoma :: #import "Tacoma"; Tacoma_Screenshot :: struct { image : sg_image; width : s32; height : s32; valid : bool = false; } current_screenshot : Tacoma_Screenshot; RDM_Atlas_Entry :: struct { world_pos: Vector3; roughness: s32; x, y: s32; // position in atlas (pixels) w, h: s32; // size of this RDM } // Per-chunk atlas bake state (CPU-side, used during baking only). RDM_Chunk_Bake :: struct { data: *float; // CPU atlas buffer (RGBA32F) width: s32; height: s32; cursor_x: s32; cursor_y: s32; row_height: s32; entries: [..]RDM_Atlas_Entry; } RDM_ATLAS_SIZE :: 4096; RDM_LOOKUP_SIZE :: 512; // 512x512 = 32*32*32*8 = 262144 texels rdm_chunk_bakes : Table(Chunk_Key, RDM_Chunk_Bake, chunk_key_hash, chunk_key_compare); ctx : *Tacoma.Tacoma_Context; // --- Chunk RDM bake queue --- // A single RDM render job: one world_trile at one roughness level. RDM_Bake_Job :: struct { world_trile_index: s32; roughness: s32; world_pos: Vector3; } RDM_Bake_State :: struct { active : bool = false; quality : s32 = 100; jobs : [..]RDM_Bake_Job; current_job : s32 = 0; } rdm_bake : RDM_Bake_State; // Get the set of roughness values present in a trile's trixels. // Always includes 7 (used for diffuse light). Returns a bitmask. get_trile_roughness_set :: (trile_name: string) -> u8 { trile := get_trile(trile_name); mask : u8 = 1 << 7; // Always include roughness 7. for x: 0..15 { for y: 0..15 { for z: 0..15 { if !trile.trixels[x][y][z].empty { mask |= cast(u8)(1 << trile.trixels[x][y][z].material.roughness); } } } } return mask; } // Build the Tacoma scene from the world, and emit bake jobs for selected chunks. // If chunk_keys is null, all chunks are queued. rdm_bake_start :: (world: World, quality: s32, include_water: bool, chunk_keys: []Chunk_Key = .[]) { if rdm_bake.active then return; trile_list : [..]Tacoma.Trile_Data; trile_list.allocator = temp; world_triles : [..]Tacoma.World_Trile; world_triles.allocator = temp; trile_name_to_index: Table(string, s32); trile_name_to_index.allocator = temp; // Per world_trile: which roughnesses to bake. world_trile_roughnesses : [..]u8; world_trile_roughnesses.allocator = temp; // Cache roughness sets per trile type. roughness_cache: Table(string, u8); roughness_cache.allocator = temp; bake_all := chunk_keys.count == 0; // Build a set of chunk keys to bake if specific ones were requested. chunk_key_set: Table(Chunk_Key, bool, chunk_key_hash, chunk_key_compare); chunk_key_set.allocator = temp; if !bake_all { for key: chunk_keys { table_set(*chunk_key_set, key, true); } } for chunk: world.chunks { should_bake := bake_all || table_contains(*chunk_key_set, chunk.coord); for group: chunk.groups { success, idx := table_find(*trile_name_to_index, group.trile_name); if !success { trile := get_trile(group.trile_name); ttrile : Tacoma.Trile_Data; for x: 0..15 { for y: 0..15 { for z: 0..15 { ttrile.trixels[x][y][z] = .{ trile.trixels[x][y][z].empty, trile.trixels[x][y][z].material.color, material_encode_to_float(trile.trixels[x][y][z].material) }; } } } gfx := get_trile_gfx(group.trile_name); ttrile.vertices = gfx.vertices.data; ttrile.vertexCount = cast(s32) (gfx.vertices.count / 3); idx = cast(s32) trile_list.count; array_add(*trile_list, ttrile); table_set(*trile_name_to_index, group.trile_name, idx); } // Get roughness set for this trile type (cached). roughness_mask : u8; found_r, cached_mask := table_find(*roughness_cache, group.trile_name); if found_r { roughness_mask = cached_mask; } else { roughness_mask = get_trile_roughness_set(group.trile_name); table_set(*roughness_cache, group.trile_name, roughness_mask); } for inst: group.instances { world_trile_idx := cast(s32) world_triles.count; wx, wy, wz := chunk_local_to_world(chunk.coord, inst.x, inst.y, inst.z); wpos := Vector3.{cast(float) wx, cast(float) wy, cast(float) wz}; array_add(*world_triles, Tacoma.World_Trile.{idx, wpos}); array_add(*world_trile_roughnesses, roughness_mask); if should_bake { // Emit one job per roughness level present. for r: 0..7 { if roughness_mask & cast(u8)(1 << r) { array_add(*rdm_bake.jobs, .{world_trile_index = world_trile_idx, roughness = cast(s32) r, world_pos = wpos}); } } } } } } sky : Tacoma.Sky_Config; sky.skyBase = world.conf.skyBase; sky.skyTop = world.conf.skyTop; sky.sunDisk = world.conf.sunDisk; sky.horizonHalo = world.conf.horizonHalo; sky.sunHalo = world.conf.sunHalo; sky.sunLightColor = world.conf.sunLightColor; sky.sunPosition = world.conf.sunPosition; sky.sunIntensity = world.conf.sunIntensity; sky.skyIntensity = world.conf.skyIntensity; blases : Tacoma.Trile_Set = .{trile_list.data, cast(s32)trile_list.count}; tlas : Tacoma.World = .{world_triles.data, cast(s32)world_triles.count}; ctx = Tacoma.tacoma_init("./modules/Tacoma/"); Tacoma.tacoma_load_scene(ctx, sky, blases, tlas, cast(s32) include_water); // Sort jobs by roughness ascending (lowest roughness = biggest images first). // Simple insertion sort, N is small. if rdm_bake.jobs.count > 1 { for i: 1..cast(s32)(rdm_bake.jobs.count - 1) { key := rdm_bake.jobs[i]; j := i - 1; while j >= 0 && rdm_bake.jobs[j].roughness > key.roughness { rdm_bake.jobs[j + 1] = rdm_bake.jobs[j]; j -= 1; } rdm_bake.jobs[j + 1] = key; } } // Clean up any previous per-chunk bake data. rdm_cleanup_chunk_bakes(); // Clean up any previous RDM results stored in chunks. curworld := get_current_world(); for *chunk: curworld.world.chunks { if chunk.rdm_valid { sg_destroy_image(chunk.rdm_atlas); sg_destroy_image(chunk.rdm_lookup); chunk.rdm_valid = false; } } // Pre-allocate per-chunk atlas CPU buffers. atlas_bytes := cast(s64) RDM_ATLAS_SIZE * cast(s64) RDM_ATLAS_SIZE * 4 * size_of(float); for job: rdm_bake.jobs { chunk_key := world_to_chunk_coord(cast(s32) job.world_pos.x, cast(s32) job.world_pos.y, cast(s32) job.world_pos.z); if !table_contains(*rdm_chunk_bakes, chunk_key) { bake : RDM_Chunk_Bake; bake.width = RDM_ATLAS_SIZE; bake.height = RDM_ATLAS_SIZE; bake.data = cast(*float) alloc(atlas_bytes); memset(bake.data, 0, atlas_bytes); table_set(*rdm_chunk_bakes, chunk_key, bake); } } rdm_bake.active = true; rdm_bake.quality = quality; if rdm_bake.jobs.count == 0 { rdm_bake_finish(); } } // Queue all chunks for RDM baking. rdm_bake_all_chunks :: (world: World, quality: s32, include_water: bool) { rdm_bake_start(world, quality, include_water); } // Queue specific chunks for RDM baking. rdm_bake_chunks :: (chunk_keys: []Chunk_Key, world: World, quality: s32, include_water: bool) { rdm_bake_start(world, quality, include_water, chunk_keys); } // Called once per frame to process at most one RDM. rdm_bake_tick :: () { if !rdm_bake.active then return; if rdm_bake.current_job >= cast(s32) rdm_bake.jobs.count { rdm_bake_finish(); return; } job := rdm_bake.jobs[rdm_bake.current_job]; w, h : s32; ptr := Tacoma.tacoma_render_rdm(ctx, job.world_trile_index, job.roughness, rdm_bake.quality, *w, *h); // Find this job's per-chunk bake state. chunk_key := world_to_chunk_coord(cast(s32) job.world_pos.x, cast(s32) job.world_pos.y, cast(s32) job.world_pos.z); bake := table_find_pointer(*rdm_chunk_bakes, chunk_key); if bake != null { // Shelf-pack this RDM into the chunk's atlas. if bake.cursor_x + w > bake.width { bake.cursor_y += bake.row_height; bake.cursor_x = 0; bake.row_height = 0; } ax := bake.cursor_x; ay := bake.cursor_y; if ay + h <= bake.height { // Copy pixels row-by-row into the chunk's atlas CPU buffer. src := cast(*u8) ptr; row_bytes := cast(s64) w * 4 * size_of(float); for row: 0..h-1 { dst_offset := (cast(s64)(ay + row) * cast(s64) bake.width + cast(s64) ax) * 4 * size_of(float); src_offset := cast(s64) row * row_bytes; memcpy(cast(*u8) bake.data + dst_offset, src + src_offset, row_bytes); } entry : RDM_Atlas_Entry; entry.world_pos = job.world_pos; entry.roughness = job.roughness; entry.x = ax; entry.y = ay; entry.w = w; entry.h = h; array_add(*bake.entries, entry); } else { print("Warning: RDM atlas overflow for chunk %, skipping (pos=%, roughness=%)\n", chunk_key, job.world_pos, job.roughness); } bake.cursor_x += w; if h > bake.row_height then bake.row_height = h; } // Still update the preview screenshot. tacoma_handle_result(ptr, w, h); rdm_bake.current_job += 1; } rdm_bake_finish :: () { if ctx != null then tacoma_stop(); curworld := get_current_world(); total_entries : s64 = 0; chunk_count : s64 = 0; // Collect unique chunk keys from jobs. bake_chunk_keys : [..]Chunk_Key; bake_chunk_keys.allocator = temp; for job: rdm_bake.jobs { ck := world_to_chunk_coord(cast(s32) job.world_pos.x, cast(s32) job.world_pos.y, cast(s32) job.world_pos.z); already := false; for bake_chunk_keys { if it == ck { already = true; break; } } if !already then array_add(*bake_chunk_keys, ck); } lookup_texels :: RDM_LOOKUP_SIZE * RDM_LOOKUP_SIZE; lookup_bytes :: lookup_texels * 4 * size_of(float); for chunk_key: bake_chunk_keys { bake := table_find_pointer(*rdm_chunk_bakes, chunk_key); if bake == null || bake.entries.count == 0 then continue; // a) Upload per-chunk atlas to GPU. atlas_imgdata : sg_image_data; atlas_byte_size := cast(u64) bake.width * cast(u64) bake.height * 4 * size_of(float); atlas_imgdata.subimage[0][0] = .{bake.data, atlas_byte_size}; atlas_desc : sg_image_desc = .{ render_target = false, width = bake.width, height = bake.height, pixel_format = sg_pixel_format.RGBA32F, sample_count = 1, data = atlas_imgdata }; atlas_image := sg_make_image(*atlas_desc); // b) Generate lookup texture (512x512 RGBA32F). lookup_data := cast(*float) alloc(lookup_bytes); memset(lookup_data, 0, lookup_bytes); atlas_w := cast(float) bake.width; atlas_h := cast(float) bake.height; for entry: bake.entries { lx, ly, lz := world_to_local(cast(s32) entry.world_pos.x, cast(s32) entry.world_pos.y, cast(s32) entry.world_pos.z); index := cast(s32) lx + cast(s32) ly * 32 + cast(s32) lz * 32 * 32 + entry.roughness * 32 * 32 * 32; tx := index % RDM_LOOKUP_SIZE; ty := index / RDM_LOOKUP_SIZE; pixel_offset := (ty * RDM_LOOKUP_SIZE + tx) * 4; lookup_data[pixel_offset + 0] = cast(float) entry.x / atlas_w; lookup_data[pixel_offset + 1] = cast(float) entry.y / atlas_h; lookup_data[pixel_offset + 2] = cast(float) entry.w / atlas_w; lookup_data[pixel_offset + 3] = cast(float) entry.h / atlas_h; } lookup_imgdata : sg_image_data; lookup_imgdata.subimage[0][0] = .{lookup_data, lookup_bytes}; lookup_desc : sg_image_desc = .{ render_target = false, width = RDM_LOOKUP_SIZE, height = RDM_LOOKUP_SIZE, pixel_format = sg_pixel_format.RGBA32F, sample_count = 1, data = lookup_imgdata }; lookup_image := sg_make_image(*lookup_desc); free(lookup_data); // c) Store in Chunk. chunk := table_find_pointer(*curworld.world.chunks, chunk_key); if chunk != null { if chunk.rdm_valid { sg_destroy_image(chunk.rdm_atlas); sg_destroy_image(chunk.rdm_lookup); } chunk.rdm_atlas = atlas_image; chunk.rdm_lookup = lookup_image; chunk.rdm_valid = true; } total_entries += cast(s64) bake.entries.count; chunk_count += 1; } print("RDM bake complete: % chunks, % total entries\n", chunk_count, total_entries); // Save baked RDM data to disk. rdm_save_all_chunks_to_disk(); // Clean up CPU bake data. rdm_cleanup_chunk_bakes(); array_free(rdm_bake.jobs); rdm_bake = .{}; } rdm_cleanup_chunk_bakes :: () { for *bake: rdm_chunk_bakes { if bake.data != null then free(bake.data); array_free(bake.entries); } deinit(*rdm_chunk_bakes); rdm_chunk_bakes = .{}; } tacoma_start :: (world: World, include_water: bool) { // Trile BLASes. trile_list : [..]Tacoma.Trile_Data; trile_list.allocator = temp; // BLAS instances to create TLAS. world_triles : [..]Tacoma.World_Trile; world_triles.allocator = temp; // Build trile type list and gather world positions from chunks. trile_name_to_index: Table(string, s32); trile_name_to_index.allocator = temp; for chunk: world.chunks { for group: chunk.groups { // Ensure this trile type is in the list. success, idx := table_find(*trile_name_to_index, group.trile_name); if !success { trile := get_trile(group.trile_name); ttrile : Tacoma.Trile_Data; for x: 0..15 { for y: 0..15 { for z: 0..15 { ttrile.trixels[x][y][z] = .{ trile.trixels[x][y][z].empty, trile.trixels[x][y][z].material.color, material_encode_to_float(trile.trixels[x][y][z].material) }; } } } gfx := get_trile_gfx(group.trile_name); ttrile.vertices = gfx.vertices.data; ttrile.vertexCount = cast(s32) (gfx.vertices.count / 3); idx = cast(s32) trile_list.count; array_add(*trile_list, ttrile); table_set(*trile_name_to_index, group.trile_name, idx); } for inst: group.instances { wx, wy, wz := chunk_local_to_world(chunk.coord, inst.x, inst.y, inst.z); array_add(*world_triles, Tacoma.World_Trile.{idx, Vector3.{cast(float) wx, cast(float) wy, cast(float) wz}}); } } } sky : Tacoma.Sky_Config; sky.skyBase = world.conf.skyBase; sky.skyTop = world.conf.skyTop; sky.sunDisk = world.conf.sunDisk; sky.horizonHalo = world.conf.horizonHalo; sky.sunHalo = world.conf.sunHalo; sky.sunLightColor = world.conf.sunLightColor; sky.sunPosition = world.conf.sunPosition; sky.sunIntensity = world.conf.sunIntensity; sky.skyIntensity = world.conf.skyIntensity; blases : Tacoma.Trile_Set = .{trile_list.data, cast(s32)trile_list.count}; for world_triles { print("World trile %\n", it); } tlas : Tacoma.World = .{world_triles.data, cast(s32)world_triles.count}; ctx = Tacoma.tacoma_init("./modules/Tacoma/"); print("CTX: %\n\n", ctx); Tacoma.tacoma_load_scene(ctx, sky, blases, tlas, cast(s32) include_water); } tacoma_stop :: () { Tacoma.tacoma_destroy(ctx); } tacoma_handle_result :: (ptr: *float, w: s32, h: s32) { data := cast(*float) talloc(w*h*4*size_of(float)); memcpy(data, ptr, w*h*4*4); for 0..(w*h) { color : Vector3; color.x = data[it * 4 + 0]; color.y = data[it * 4 + 1]; color.z = data[it * 4 + 2]; data[it * 4 + 0] = color.x; data[it * 4 + 1] = color.y; data[it * 4 + 2] = color.z; } imgdata : sg_image_data; imgdata.subimage[0][0] = .{data, cast(u64) (w*h*4*4)}; texdesc : sg_image_desc = .{ render_target = false, width = w, height = h, pixel_format = sg_pixel_format.RGBA32F, sample_count = 1, data = imgdata }; if current_screenshot.valid { sg_destroy_image(current_screenshot.image); } current_screenshot = .{ sg_make_image(*texdesc), w, h, true }; Tacoma.tacoma_free_result(ptr); } gen_reference :: (w: s32, h: s32, eye: Vector3, target: Vector3, quality: s32, include_water: bool, world: World) { tacoma_start(world, include_water); ptr := Tacoma.tacoma_render_reference(ctx, w, h, eye, target, 0.01, quality); tacoma_handle_result(ptr, w, h); tacoma_stop(); } gen_rdm :: (quality: s32, include_water: bool, world: World) { tacoma_start(world, include_water); w, h : s32; ptr := Tacoma.tacoma_render_rdm(ctx, 0, 0, quality, *w, *h); tacoma_handle_result(ptr, w, h); tacoma_stop(); } // --- RDM disk persistence --- #if OS != .WASM { rdm_file :: #import "File"; } RDM_File_Header :: struct { magic: u32; width: s32; height: s32; } RDM_FILE_MAGIC :: u32.[0x4D445254][0]; // "TRDM" as little-endian u32 rdm_chunk_filename :: (world_name: string, chunk_key: Chunk_Key, suffix: string) -> string { return tprint("./game/resources/worlds/%/%_%_%.%", world_name, chunk_key.x, chunk_key.y, chunk_key.z, suffix); } rdm_save_image_to_file :: (path: string, data: *float, width: s32, height: s32) { #if OS != .WASM { builder: String_Builder; header := RDM_File_Header.{ magic = RDM_FILE_MAGIC, width = width, height = height, }; write_bytes(*builder, *header, size_of(RDM_File_Header)); pixel_bytes := cast(s64) width * cast(s64) height * 4 * size_of(float); write_bytes(*builder, data, pixel_bytes); rdm_file.write_entire_file(path, builder_to_string(*builder)); } } rdm_save_all_chunks_to_disk :: () { #if OS != .WASM { curworld := get_current_world(); if !curworld.valid then return; world_name := curworld.world.name; for *bake, chunk_key: rdm_chunk_bakes { if bake.entries.count == 0 then continue; // Save atlas. atlas_path := rdm_chunk_filename(world_name, chunk_key, "rdm_atlas"); rdm_save_image_to_file(atlas_path, bake.data, bake.width, bake.height); // Regenerate and save lookup. lookup_texels :: RDM_LOOKUP_SIZE * RDM_LOOKUP_SIZE; lookup_floats :: lookup_texels * 4; lookup_data : [lookup_floats]float; memset(lookup_data.data, 0, size_of(type_of(lookup_data))); atlas_w := cast(float) bake.width; atlas_h := cast(float) bake.height; for entry: bake.entries { lx, ly, lz := world_to_local(cast(s32) entry.world_pos.x, cast(s32) entry.world_pos.y, cast(s32) entry.world_pos.z); index := cast(s32) lx + cast(s32) ly * 32 + cast(s32) lz * 32 * 32 + entry.roughness * 32 * 32 * 32; tx := index % RDM_LOOKUP_SIZE; ty := index / RDM_LOOKUP_SIZE; pixel_offset := (ty * RDM_LOOKUP_SIZE + tx) * 4; lookup_data[pixel_offset + 0] = cast(float) entry.x / atlas_w; lookup_data[pixel_offset + 1] = cast(float) entry.y / atlas_h; lookup_data[pixel_offset + 2] = cast(float) entry.w / atlas_w; lookup_data[pixel_offset + 3] = cast(float) entry.h / atlas_h; } lookup_path := rdm_chunk_filename(world_name, chunk_key, "rdm_lookup"); rdm_save_image_to_file(lookup_path, lookup_data.data, RDM_LOOKUP_SIZE, RDM_LOOKUP_SIZE); print("Saved RDM data for chunk % to disk\n", chunk_key); } } } rdm_load_from_disk :: () { #if OS != .WASM { curworld := get_current_world(); if !curworld.valid then return; world_name := curworld.world.name; for *chunk: curworld.world.chunks { if chunk.rdm_valid then continue; atlas_path := rdm_chunk_filename(world_name, chunk.coord, "rdm_atlas"); lookup_path := rdm_chunk_filename(world_name, chunk.coord, "rdm_lookup"); atlas_data, atlas_ok := rdm_file.read_entire_file(atlas_path); if !atlas_ok then continue; lookup_data, lookup_ok := rdm_file.read_entire_file(lookup_path); if !lookup_ok then continue; header_size := cast(s64) size_of(RDM_File_Header); if atlas_data.count < header_size || lookup_data.count < header_size then continue; atlas_header := cast(*RDM_File_Header) atlas_data.data; lookup_header := cast(*RDM_File_Header) lookup_data.data; if atlas_header.magic != RDM_FILE_MAGIC || lookup_header.magic != RDM_FILE_MAGIC then continue; // Create atlas GPU image. atlas_imgdata : sg_image_data; atlas_pixel_bytes := cast(u64) atlas_header.width * cast(u64) atlas_header.height * 4 * size_of(float); atlas_imgdata.subimage[0][0] = .{atlas_data.data + header_size, atlas_pixel_bytes}; atlas_desc : sg_image_desc = .{ render_target = false, width = atlas_header.width, height = atlas_header.height, pixel_format = sg_pixel_format.RGBA32F, sample_count = 1, data = atlas_imgdata, }; chunk.rdm_atlas = sg_make_image(*atlas_desc); // Create lookup GPU image. lookup_imgdata : sg_image_data; lookup_pixel_bytes := cast(u64) lookup_header.width * cast(u64) lookup_header.height * 4 * size_of(float); lookup_imgdata.subimage[0][0] = .{lookup_data.data + header_size, lookup_pixel_bytes}; lookup_desc : sg_image_desc = .{ render_target = false, width = lookup_header.width, height = lookup_header.height, pixel_format = sg_pixel_format.RGBA32F, sample_count = 1, data = lookup_imgdata, }; chunk.rdm_lookup = sg_make_image(*lookup_desc); chunk.rdm_valid = true; print("Loaded RDM data for chunk % from disk\n", chunk.coord); } } }