P4.3: render seeded board with real gem sprites (sx, iOS sim)
Adopt the modules/ui UIPipeline framework (as the chess reference app does) and replace the P0 placeholder quad with a BoardView (View protocol, modeled on chess/board_view.sx): - background.png fills the screen; an 8x8 cell.png grid is centered in the safe area; each cell's gem is sampled from gems.png by UV column = gem index (0=red .. 5=purple). - Drive it from board.sx seeded with 1337 (the board_init golden's seed), so the on-screen layout matches that snapshot gem-for-gem. main.sx now hosts the view via UIPipeline (Metal on iOS, GL on desktop) and heap-allocates the board/asset state behind pointers (UFCS method calls on a value-typed global mutate a copy, so mutable state must live behind a pointer as the reference app does). Vendor the C deps the UI module's image/font path needs (stb_image, stb_truetype, kb_text_shape, file_utils); their #include "vendors/..." paths resolve relative to the project root. Evidence: ios-sim build links clean; tools/run_tests.sh 11/11 pass; running app captured at goldens/p4_board.png.
This commit is contained in:
173
board_view.sx
Normal file
173
board_view.sx
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
// BoardView (P4.3) — render the seeded match-3 board with real gem sprites.
|
||||||
|
//
|
||||||
|
// Modeled on game/chess/board_view.sx: a `View` that lays out an 8×8 grid and
|
||||||
|
// draws tiles/sprites through RenderContext.add_image / add_image_uv, sampling
|
||||||
|
// the gem sprite sheet by UV column. The background image fills the whole view;
|
||||||
|
// the grid is a centered square inside the safe-area inset.
|
||||||
|
#import "modules/std.sx";
|
||||||
|
#import "modules/math";
|
||||||
|
#import "modules/opengl.sx";
|
||||||
|
#import "modules/stb.sx";
|
||||||
|
#import "modules/gpu/types.sx";
|
||||||
|
#import "modules/gpu/api.sx";
|
||||||
|
#import "modules/ui/types.sx";
|
||||||
|
#import "modules/ui/render.sx";
|
||||||
|
#import "modules/ui/events.sx";
|
||||||
|
#import "modules/ui/view.sx";
|
||||||
|
#import "board.sx";
|
||||||
|
|
||||||
|
// Fraction of a cell each gem occupies; the remainder is margin so a gem sits
|
||||||
|
// inside its cell tile rather than touching the tile's edges.
|
||||||
|
GEM_FILL_FRAC :f32: 0.84;
|
||||||
|
|
||||||
|
// UV sub-rect of one gem column, spanning the sheet's full height.
|
||||||
|
GemUV :: struct {
|
||||||
|
uv_min: Point;
|
||||||
|
uv_max: Point;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loads and holds the three board textures (background, cell tile, gem sheet)
|
||||||
|
// and maps a gem index to its column UV. Modeled on chess's ChessPieces.
|
||||||
|
BoardAssets :: struct {
|
||||||
|
bg_tex: u32;
|
||||||
|
cell_tex: u32;
|
||||||
|
gems_tex: u32;
|
||||||
|
cell_u: f32;
|
||||||
|
loaded: bool;
|
||||||
|
|
||||||
|
init :: (self: *BoardAssets) {
|
||||||
|
self.bg_tex = 0;
|
||||||
|
self.cell_tex = 0;
|
||||||
|
self.gems_tex = 0;
|
||||||
|
// gems.png is GEM_COUNT columns wide and one row tall, so a gem's UV
|
||||||
|
// column IS its gem index (0=red … 5=purple); cell_u is one column wide.
|
||||||
|
self.cell_u = 1.0 / cast(f32) GEM_COUNT;
|
||||||
|
self.loaded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
load :: (self: *BoardAssets, gpu: ?GPU) {
|
||||||
|
self.bg_tex = load_texture("assets/board/background.png", gpu);
|
||||||
|
self.cell_tex = load_texture("assets/board/cell.png", gpu);
|
||||||
|
self.gems_tex = load_texture("assets/gems/gems.png", gpu);
|
||||||
|
self.loaded = self.bg_tex != 0 and self.cell_tex != 0 and self.gems_tex != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
gem_uv :: (self: *BoardAssets, index: s64) -> GemUV {
|
||||||
|
u0 : f32 = xx index * self.cell_u;
|
||||||
|
GemUV.{
|
||||||
|
uv_min = Point.{ x = u0, y = 0.0 },
|
||||||
|
uv_max = Point.{ x = u0 + self.cell_u, y = 1.0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode an RGBA image and upload it as a texture, returning the handle (0 on
|
||||||
|
// failure). When a GPU backend is bound (iOS Metal) it owns the upload; the
|
||||||
|
// desktop GL path falls back to a plain GL_TEXTURE_2D.
|
||||||
|
load_texture :: (path: [:0]u8, gpu: ?GPU) -> u32 {
|
||||||
|
w : s32 = 0;
|
||||||
|
h : s32 = 0;
|
||||||
|
ch : s32 = 0;
|
||||||
|
pixels := stbi_load(path, @w, @h, @ch, 4);
|
||||||
|
if pixels == null {
|
||||||
|
out("WARNING: could not load texture: ");
|
||||||
|
out(path);
|
||||||
|
out("\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
tex : u32 = 0;
|
||||||
|
if gpu != null {
|
||||||
|
tex = gpu.create_texture(w, h, .rgba8, xx pixels);
|
||||||
|
} else {
|
||||||
|
glGenTextures(1, @tex);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, tex);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, xx GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, xx GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, xx GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, xx GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, xx GL_CLAMP_TO_EDGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
stbi_image_free(pixels);
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
|
BoardView :: struct {
|
||||||
|
board: *Board;
|
||||||
|
assets: *BoardAssets;
|
||||||
|
safe: EdgeInsets;
|
||||||
|
|
||||||
|
cell_size: f32;
|
||||||
|
origin: Point;
|
||||||
|
|
||||||
|
// Center a square 8×8 grid inside the safe-area-inset region of `frame`.
|
||||||
|
compute_layout :: (self: *BoardView, frame: Frame) {
|
||||||
|
avail := frame.inset(self.safe);
|
||||||
|
cols : f32 = xx BOARD_COLS;
|
||||||
|
board_dim := min(avail.size.width, avail.size.height);
|
||||||
|
self.cell_size = board_dim / cols;
|
||||||
|
total := self.cell_size * cols;
|
||||||
|
self.origin = Point.{
|
||||||
|
x = avail.origin.x + (avail.size.width - total) * 0.5,
|
||||||
|
y = avail.origin.y + (avail.size.height - total) * 0.5
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
cell_frame :: (self: *BoardView, col: s64, row: s64) -> Frame {
|
||||||
|
Frame.make(
|
||||||
|
self.origin.x + xx col * self.cell_size,
|
||||||
|
self.origin.y + xx row * self.cell_size,
|
||||||
|
self.cell_size,
|
||||||
|
self.cell_size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for BoardView {
|
||||||
|
size_that_fits :: (self: *BoardView, proposal: ProposedSize) -> Size {
|
||||||
|
Size.{ width = proposal.width ?? 0.0, height = proposal.height ?? 0.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
layout :: (self: *BoardView, bounds: Frame) {
|
||||||
|
self.compute_layout(bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
render :: (self: *BoardView, ctx: *RenderContext, frame: Frame) {
|
||||||
|
self.compute_layout(frame);
|
||||||
|
|
||||||
|
// 1. Background image fills the whole view, behind the grid.
|
||||||
|
if self.assets.bg_tex != 0 {
|
||||||
|
ctx.add_image(frame, self.assets.bg_tex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. One cell tile per board cell, then its gem sampled by index column.
|
||||||
|
gem_inset := self.cell_size * (1.0 - GEM_FILL_FRAC) * 0.5;
|
||||||
|
gem_dim := self.cell_size * GEM_FILL_FRAC;
|
||||||
|
for 0..BOARD_ROWS: (row) {
|
||||||
|
for 0..BOARD_COLS: (col) {
|
||||||
|
cf := self.cell_frame(col, row);
|
||||||
|
|
||||||
|
if self.assets.cell_tex != 0 {
|
||||||
|
ctx.add_image(cf, self.assets.cell_tex);
|
||||||
|
}
|
||||||
|
|
||||||
|
g := self.board.at(col, row);
|
||||||
|
if g != .empty and self.assets.gems_tex != 0 {
|
||||||
|
uv := self.assets.gem_uv(cast(s64) g);
|
||||||
|
gf := Frame.make(
|
||||||
|
cf.origin.x + gem_inset,
|
||||||
|
cf.origin.y + gem_inset,
|
||||||
|
gem_dim,
|
||||||
|
gem_dim
|
||||||
|
);
|
||||||
|
ctx.add_image_uv(gf, self.assets.gems_tex, uv.uv_min, uv.uv_max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_event :: (self: *BoardView, event: *Event, frame: Frame) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
goldens/p4_board.png
Normal file
BIN
goldens/p4_board.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 MiB |
171
main.sx
171
main.sx
@@ -3,113 +3,60 @@
|
|||||||
#import "modules/compiler.sx";
|
#import "modules/compiler.sx";
|
||||||
#import "modules/opengl.sx";
|
#import "modules/opengl.sx";
|
||||||
#import "modules/sdl3.sx";
|
#import "modules/sdl3.sx";
|
||||||
|
#import "modules/math";
|
||||||
|
#import "modules/stb.sx";
|
||||||
|
#import "modules/stb_truetype.sx";
|
||||||
#import "modules/gpu/api.sx";
|
#import "modules/gpu/api.sx";
|
||||||
#import "modules/gpu/types.sx";
|
#import "modules/gpu/types.sx";
|
||||||
#import "modules/gpu/metal.sx";
|
#import "modules/gpu/metal.sx";
|
||||||
|
#import "modules/ui";
|
||||||
#import "modules/platform/api.sx";
|
#import "modules/platform/api.sx";
|
||||||
#import "modules/platform/sdl3.sx";
|
#import "modules/platform/sdl3.sx";
|
||||||
#import "modules/platform/uikit.sx";
|
#import "modules/platform/uikit.sx";
|
||||||
|
#import "board.sx";
|
||||||
|
#import "board_view.sx";
|
||||||
|
|
||||||
#run configure_build();
|
#run configure_build();
|
||||||
|
|
||||||
|
// Fixed seed for the rendered board — the same seed tests/board_init.sx locks
|
||||||
|
// as a snapshot, so the on-screen layout matches that golden gem-for-gem.
|
||||||
|
BOARD_SEED :: 1337;
|
||||||
|
|
||||||
g_plat : Platform = ---;
|
g_plat : Platform = ---;
|
||||||
|
g_pipeline : *UIPipeline = ---;
|
||||||
g_viewport_w : f32 = 800.0;
|
g_viewport_w : f32 = 800.0;
|
||||||
g_viewport_h : f32 = 600.0;
|
g_viewport_h : f32 = 600.0;
|
||||||
|
g_safe_insets : EdgeInsets = .{};
|
||||||
|
|
||||||
// iOS-only concrete handles kept alongside the boxed `g_plat` so the frame
|
// iOS-only concrete handles kept alongside the boxed `g_plat` so the frame loop
|
||||||
// loop can reach the CAMetalLayer pointer / pixel dims without going through
|
// can reach the CAMetalLayer pointer / pixel dims without going through the
|
||||||
// the protocol box.
|
// protocol box.
|
||||||
g_uikit_plat : *UIKitPlatform = null;
|
g_uikit_plat : *UIKitPlatform = null;
|
||||||
g_metal_gpu : *MetalGPU = null;
|
g_metal_gpu : *MetalGPU = null;
|
||||||
|
|
||||||
// ── Render-path proof (P0.2) ─────────────────────────────────────────────
|
// The pure-sx model (board.sx) and its sprites, seeded once in main() and
|
||||||
// Clear to a solid blue, then draw one centered orange quad covering the
|
// rendered every frame. Heap-allocated so the view holds stable pointers to
|
||||||
// central 50%×50% of the drawable. Geometry is in NDC ([-0.5, 0.5]²) so the
|
// the mutable state across frames.
|
||||||
// quad stays screen-size independent across simulator devices, which keeps
|
g_board : *Board = null;
|
||||||
// the screenshot golden unambiguous. This is the GPU protocol's
|
g_assets : *BoardAssets = null;
|
||||||
// clear+quad path: an MSL pipeline state plus a 6-vertex (2-triangle)
|
|
||||||
// buffer, created lazily once the MTLDevice exists.
|
|
||||||
g_quad_shader : ShaderHandle = 0;
|
|
||||||
g_quad_vbuf : BufferHandle = 0;
|
|
||||||
|
|
||||||
// ── Input-path proof (P0.3) ──────────────────────────────────────────────
|
// Rebuilt each frame inside the pipeline's arena; carries the current safe-area
|
||||||
// A tap toggles the quad between two distinct colors, proving a real touch
|
// insets so the grid stays inside the notch / home-indicator region.
|
||||||
// reaches sx and changes the rendered frame. UIKit touchesBegan is mapped to
|
build_ui :: () -> View {
|
||||||
// a `mouse_down` Event in uikit.sx; the frame loop's poll flips
|
BoardView.{ board = g_board, assets = g_assets, safe = g_safe_insets }
|
||||||
// `g_quad_flipped` and marks `g_quad_dirty`, which re-uploads the matching
|
|
||||||
// vertex colors before the next draw.
|
|
||||||
g_quad_flipped : bool = false;
|
|
||||||
g_quad_dirty : bool = true;
|
|
||||||
|
|
||||||
BG_CLEAR :: ClearColor.{ r = 0.10, g = 0.20, b = 0.55, a = 1.0 };
|
|
||||||
|
|
||||||
// Vertex layout matches QUAD_MSL's `Vertex`: packed_float2 pos +
|
|
||||||
// packed_float4 color = 24 bytes. `packed_*` avoids the 16-byte alignment
|
|
||||||
// padding a plain `float4` would force. 6 vertices = 2 triangles. Two
|
|
||||||
// arrays share the same geometry and differ only in color so the buffer
|
|
||||||
// re-upload is a flat memcpy of the active palette.
|
|
||||||
QUAD_VERTS_ORANGE : [36]f32 = .[
|
|
||||||
-0.5, 0.5, 1.0, 0.6, 0.0, 1.0,
|
|
||||||
0.5, 0.5, 1.0, 0.6, 0.0, 1.0,
|
|
||||||
-0.5, -0.5, 1.0, 0.6, 0.0, 1.0,
|
|
||||||
0.5, 0.5, 1.0, 0.6, 0.0, 1.0,
|
|
||||||
0.5, -0.5, 1.0, 0.6, 0.0, 1.0,
|
|
||||||
-0.5, -0.5, 1.0, 0.6, 0.0, 1.0,
|
|
||||||
];
|
|
||||||
|
|
||||||
QUAD_VERTS_GREEN : [36]f32 = .[
|
|
||||||
-0.5, 0.5, 0.15, 0.85, 0.35, 1.0,
|
|
||||||
0.5, 0.5, 0.15, 0.85, 0.35, 1.0,
|
|
||||||
-0.5, -0.5, 0.15, 0.85, 0.35, 1.0,
|
|
||||||
0.5, 0.5, 0.15, 0.85, 0.35, 1.0,
|
|
||||||
0.5, -0.5, 0.15, 0.85, 0.35, 1.0,
|
|
||||||
-0.5, -0.5, 0.15, 0.85, 0.35, 1.0,
|
|
||||||
];
|
|
||||||
|
|
||||||
// Pass-through shader: the vertex stage emits NDC positions directly (no
|
|
||||||
// projection), the fragment stage returns the interpolated vertex color.
|
|
||||||
// Entry-point names vmain / fmain are what MetalGPU.create_shader looks up.
|
|
||||||
QUAD_MSL :: #string MSL
|
|
||||||
#include <metal_stdlib>
|
|
||||||
using namespace metal;
|
|
||||||
|
|
||||||
struct Vertex {
|
|
||||||
packed_float2 pos;
|
|
||||||
packed_float4 color;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct RasterizerData {
|
|
||||||
float4 position [[position]];
|
|
||||||
float4 color;
|
|
||||||
};
|
|
||||||
|
|
||||||
vertex RasterizerData vmain(uint vid [[vertex_id]],
|
|
||||||
constant Vertex* vertices [[buffer(0)]]) {
|
|
||||||
RasterizerData out;
|
|
||||||
out.position = float4(vertices[vid].pos, 0.0, 1.0);
|
|
||||||
out.color = float4(vertices[vid].color);
|
|
||||||
return out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fragment float4 fmain(RasterizerData in [[stage_in]]) {
|
|
||||||
return in.color;
|
|
||||||
}
|
|
||||||
MSL;
|
|
||||||
|
|
||||||
frame :: () {
|
frame :: () {
|
||||||
fc := g_plat.begin_frame();
|
fc := g_plat.begin_frame();
|
||||||
g_viewport_w = fc.viewport_w;
|
g_viewport_w = fc.viewport_w;
|
||||||
g_viewport_h = fc.viewport_h;
|
g_viewport_h = fc.viewport_h;
|
||||||
|
g_safe_insets = g_plat.safe_insets();
|
||||||
|
|
||||||
|
if fc.viewport_w != g_pipeline.screen_width or fc.viewport_h != g_pipeline.screen_height {
|
||||||
|
g_pipeline.resize(fc.viewport_w, fc.viewport_h);
|
||||||
|
}
|
||||||
|
|
||||||
for g_plat.poll_events(): (*ev) {
|
for g_plat.poll_events(): (*ev) {
|
||||||
if ev == {
|
|
||||||
// Flip on the press only. A tap also produces mouse_up
|
|
||||||
// (touchesEnded); toggling on both would net to no change.
|
|
||||||
case .mouse_down: {
|
|
||||||
g_quad_flipped = !g_quad_flipped;
|
|
||||||
g_quad_dirty = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
inline if OS != .ios {
|
inline if OS != .ios {
|
||||||
if ev == {
|
if ev == {
|
||||||
case .key_up: (e) {
|
case .key_up: (e) {
|
||||||
@@ -117,13 +64,14 @@ frame :: () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
g_pipeline.dispatch_event(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline if OS == .ios {
|
inline if OS == .ios {
|
||||||
// Lazy-attach Metal once -[SxAppDelegate didFinishLaunching:] has
|
// Lazy-attach Metal once -[SxAppDelegate didFinishLaunching:] has
|
||||||
// installed the SxMetalView and its bounds have been measured; both
|
// installed the SxMetalView and its bounds have been measured; both can
|
||||||
// can lag the first CADisplayLink tick, and a zero-sized drawable
|
// lag the first CADisplayLink tick, and a zero-sized drawable aborts
|
||||||
// aborts via XPC.
|
// via XPC.
|
||||||
if g_uikit_plat.gl_layer == null { return; }
|
if g_uikit_plat.gl_layer == null { return; }
|
||||||
if g_uikit_plat.pixel_w <= 0 or g_uikit_plat.pixel_h <= 0 { return; }
|
if g_uikit_plat.pixel_w <= 0 or g_uikit_plat.pixel_h <= 0 { return; }
|
||||||
if g_metal_gpu.layer == null {
|
if g_metal_gpu.layer == null {
|
||||||
@@ -131,31 +79,15 @@ frame :: () {
|
|||||||
} else if g_metal_gpu.pixel_w != g_uikit_plat.pixel_w or g_metal_gpu.pixel_h != g_uikit_plat.pixel_h {
|
} else if g_metal_gpu.pixel_w != g_uikit_plat.pixel_w or g_metal_gpu.pixel_h != g_uikit_plat.pixel_h {
|
||||||
g_metal_gpu.resize(g_uikit_plat.pixel_w, g_uikit_plat.pixel_h);
|
g_metal_gpu.resize(g_uikit_plat.pixel_w, g_uikit_plat.pixel_h);
|
||||||
}
|
}
|
||||||
// Compile the quad pipeline + upload its vertices once. The MTLDevice
|
clear : ClearColor = .{ r = 0.05, g = 0.06, b = 0.10, a = 1.0 };
|
||||||
// was created eagerly in main(), so both only need a valid device.
|
if !g_metal_gpu.begin_frame(clear) { return; }
|
||||||
if g_quad_shader == 0 {
|
g_pipeline.tick();
|
||||||
g_quad_shader = g_metal_gpu.create_shader(QUAD_MSL, "");
|
|
||||||
if g_quad_shader == 0 { return; }
|
|
||||||
}
|
|
||||||
if g_quad_vbuf == 0 {
|
|
||||||
g_quad_vbuf = g_metal_gpu.create_buffer(size_of([36]f32));
|
|
||||||
if g_quad_vbuf == 0 { return; }
|
|
||||||
}
|
|
||||||
if g_quad_dirty {
|
|
||||||
verts := if g_quad_flipped then @QUAD_VERTS_GREEN else @QUAD_VERTS_ORANGE;
|
|
||||||
g_metal_gpu.update_buffer(g_quad_vbuf, xx verts, size_of([36]f32));
|
|
||||||
g_quad_dirty = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !g_metal_gpu.begin_frame(BG_CLEAR) { return; }
|
|
||||||
g_metal_gpu.set_shader(g_quad_shader);
|
|
||||||
g_metal_gpu.set_vertex_buffer(g_quad_vbuf);
|
|
||||||
g_metal_gpu.draw_triangles(0, 6);
|
|
||||||
g_metal_gpu.end_frame(fc.target_present_time);
|
g_metal_gpu.end_frame(fc.target_present_time);
|
||||||
} else {
|
} else {
|
||||||
glViewport(0, 0, fc.pixel_w, fc.pixel_h);
|
glViewport(0, 0, fc.pixel_w, fc.pixel_h);
|
||||||
glClearColor(0.10, 0.20, 0.55, 1.0);
|
glClearColor(0.05, 0.06, 0.10, 1.0);
|
||||||
glClear(GL_COLOR_BUFFER_BIT);
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
g_pipeline.tick();
|
||||||
}
|
}
|
||||||
g_plat.end_frame();
|
g_plat.end_frame();
|
||||||
}
|
}
|
||||||
@@ -168,9 +100,10 @@ main :: () -> void {
|
|||||||
g_plat = xx u;
|
g_plat = xx u;
|
||||||
g_uikit_plat = u;
|
g_uikit_plat = u;
|
||||||
|
|
||||||
// The CAMetalLayer doesn't exist until didFinishLaunching: runs after
|
// The CAMetalLayer doesn't exist until didFinishLaunching: runs after we
|
||||||
// we return into UIApplicationMain, so attach lazily on the first
|
// return into UIApplicationMain, so attach lazily on the first frame.
|
||||||
// frame. init(null, 0, 0) only needs the MTLDevice.
|
// init(null, 0, 0) only needs the MTLDevice, which is enough for the
|
||||||
|
// texture uploads below.
|
||||||
g_metal_gpu = xx context.allocator.alloc(size_of(MetalGPU));
|
g_metal_gpu = xx context.allocator.alloc(size_of(MetalGPU));
|
||||||
// alloc returns uninitialized memory; struct field defaults are NOT
|
// alloc returns uninitialized memory; struct field defaults are NOT
|
||||||
// applied, so List caps/lens would be garbage without this memset.
|
// applied, so List caps/lens would be garbage without this memset.
|
||||||
@@ -185,6 +118,26 @@ main :: () -> void {
|
|||||||
fc := g_plat.begin_frame();
|
fc := g_plat.begin_frame();
|
||||||
g_viewport_w = fc.viewport_w;
|
g_viewport_w = fc.viewport_w;
|
||||||
g_viewport_h = fc.viewport_h;
|
g_viewport_h = fc.viewport_h;
|
||||||
|
g_safe_insets = g_plat.safe_insets();
|
||||||
|
|
||||||
|
g_pipeline = xx context.allocator.alloc(size_of(UIPipeline));
|
||||||
|
// Same alloc caveat as above: zero so the optional `gpu` reads as null on
|
||||||
|
// the desktop path (where set_gpu is not called) and the Lists start empty.
|
||||||
|
memset(xx g_pipeline, 0, size_of(UIPipeline));
|
||||||
|
inline if OS == .ios {
|
||||||
|
g_pipeline.set_gpu(xx g_metal_gpu);
|
||||||
|
}
|
||||||
|
g_pipeline.init(fc.viewport_w, fc.viewport_h);
|
||||||
|
g_pipeline.init_font("assets/fonts/default.ttf", 32.0, fc.dpi_scale);
|
||||||
|
|
||||||
|
g_board = xx context.allocator.alloc(size_of(Board));
|
||||||
|
g_board.init(BOARD_SEED);
|
||||||
|
|
||||||
|
g_assets = xx context.allocator.alloc(size_of(BoardAssets));
|
||||||
|
g_assets.init();
|
||||||
|
g_assets.load(g_pipeline.gpu);
|
||||||
|
|
||||||
|
g_pipeline.set_body(closure(build_ui));
|
||||||
|
|
||||||
g_plat.run_frame_loop(closure(frame));
|
g_plat.run_frame_loop(closure(frame));
|
||||||
g_plat.shutdown();
|
g_plat.shutdown();
|
||||||
|
|||||||
55
vendors/file_utils/file_utils.c
vendored
Normal file
55
vendors/file_utils/file_utils.c
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
#include <android/asset_manager.h>
|
||||||
|
|
||||||
|
// Caller-installed AAssetManager pointer. Chess's android_main extracts
|
||||||
|
// it from `app->activity->assetManager` (via sx-side platform module's
|
||||||
|
// `g_android_asset_manager` global) and feeds it here once at startup.
|
||||||
|
// Until the setter has been called, Android falls through to fopen —
|
||||||
|
// gives a predictable "file not found" rather than a NULL-deref.
|
||||||
|
static AAssetManager* g_aam = NULL;
|
||||||
|
|
||||||
|
void sx_android_set_asset_manager(void* m) {
|
||||||
|
g_aam = (AAssetManager*)m;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
unsigned char* read_file_bytes(const char* path, int* out_size) {
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
if (g_aam != NULL) {
|
||||||
|
// AAssetManager paths are relative to the APK's `assets/`
|
||||||
|
// directory. Strip a leading "assets/" so callers can use the
|
||||||
|
// same paths across iOS/macOS/Android (those platforms read
|
||||||
|
// assets via `assets/...` rooted in the bundle or CWD).
|
||||||
|
const char* lookup = path;
|
||||||
|
if (strncmp(path, "assets/", 7) == 0) {
|
||||||
|
lookup = path + 7;
|
||||||
|
}
|
||||||
|
AAsset* a = AAssetManager_open(g_aam, lookup, AASSET_MODE_BUFFER);
|
||||||
|
if (a != NULL) {
|
||||||
|
off_t n = AAsset_getLength(a);
|
||||||
|
*out_size = (int)n;
|
||||||
|
unsigned char* buf = (unsigned char*)malloc((size_t)n);
|
||||||
|
if (buf != NULL) {
|
||||||
|
memcpy(buf, AAsset_getBuffer(a), (size_t)n);
|
||||||
|
}
|
||||||
|
AAsset_close(a);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
// Falls through to fopen — useful when assets land in the data
|
||||||
|
// dir via extraction or app updates.
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
FILE* f = fopen(path, "rb");
|
||||||
|
if (!f) return 0;
|
||||||
|
fseek(f, 0, SEEK_END);
|
||||||
|
*out_size = (int)ftell(f);
|
||||||
|
fseek(f, 0, SEEK_SET);
|
||||||
|
unsigned char* buf = (unsigned char*)malloc(*out_size);
|
||||||
|
fread(buf, 1, *out_size, f);
|
||||||
|
fclose(f);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
13
vendors/file_utils/file_utils.h
vendored
Normal file
13
vendors/file_utils/file_utils.h
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#ifndef FILE_UTILS_H
|
||||||
|
#define FILE_UTILS_H
|
||||||
|
|
||||||
|
unsigned char* read_file_bytes(const char* path, int* out_size);
|
||||||
|
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
// Install the AAssetManager that `read_file_bytes` consults for paths
|
||||||
|
// rooted inside the APK. Caller is responsible for passing the manager
|
||||||
|
// from `ANativeActivity->assetManager` before any read_file_bytes call.
|
||||||
|
void sx_android_set_asset_manager(void* m);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
19
vendors/kb_text_shape/kb/LICENSE
vendored
Normal file
19
vendors/kb_text_shape/kb/LICENSE
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
zlib License
|
||||||
|
|
||||||
|
(C) Copyright 2024-2025 Jimmy Lefevre
|
||||||
|
|
||||||
|
This software is provided 'as-is', without any express or implied
|
||||||
|
warranty. In no event will the authors be held liable for any damages
|
||||||
|
arising from the use of this software.
|
||||||
|
|
||||||
|
Permission is granted to anyone to use this software for any purpose,
|
||||||
|
including commercial applications, and to alter it and redistribute it
|
||||||
|
freely, subject to the following restrictions:
|
||||||
|
|
||||||
|
1. The origin of this software must not be misrepresented; you must not
|
||||||
|
claim that you wrote the original software. If you use this software
|
||||||
|
in a product, an acknowledgment in the product documentation would be
|
||||||
|
appreciated but is not required.
|
||||||
|
2. Altered source versions must be plainly marked as such, and must not be
|
||||||
|
misrepresented as being the original software.
|
||||||
|
3. This notice may not be removed or altered from any source distribution.
|
||||||
30737
vendors/kb_text_shape/kb/kb_text_shape.h
vendored
Normal file
30737
vendors/kb_text_shape/kb/kb_text_shape.h
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2
vendors/kb_text_shape/kb_text_shape_impl.c
vendored
Normal file
2
vendors/kb_text_shape/kb_text_shape_impl.c
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#define KB_TEXT_SHAPE_IMPLEMENTATION
|
||||||
|
#include "kb/kb_text_shape.h"
|
||||||
15
vendors/kb_text_shape/kbts_api.h
vendored
Normal file
15
vendors/kb_text_shape/kbts_api.h
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// Minimal API declarations for SX import.
|
||||||
|
// Only the functions/types we actually use — avoids parsing the full 30k-line header.
|
||||||
|
|
||||||
|
typedef struct kbts_shape_context kbts_shape_context;
|
||||||
|
typedef struct kbts_font kbts_font;
|
||||||
|
|
||||||
|
kbts_shape_context *kbts_CreateShapeContext(void *Allocator, void *AllocatorData);
|
||||||
|
void kbts_DestroyShapeContext(kbts_shape_context *Context);
|
||||||
|
kbts_font *kbts_ShapePushFontFromMemory(kbts_shape_context *Context, void *Memory, int Size, int FontIndex);
|
||||||
|
void kbts_GetFontInfo2(kbts_font *Font, void *Info);
|
||||||
|
void kbts_ShapeBegin(kbts_shape_context *Context, unsigned int ParagraphDirection, unsigned int Language);
|
||||||
|
void kbts_ShapeUtf8(kbts_shape_context *Context, const char *Utf8, int Length, unsigned int UserIdGenerationMode);
|
||||||
|
void kbts_ShapeEnd(kbts_shape_context *Context);
|
||||||
|
int kbts_ShapeRun(kbts_shape_context *Context, void *Run);
|
||||||
|
int kbts_GlyphIteratorNext(void *It, void **Glyph);
|
||||||
7988
vendors/stb_image/stb_image.h
vendored
Normal file
7988
vendors/stb_image/stb_image.h
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2
vendors/stb_image/stb_image_impl.c
vendored
Normal file
2
vendors/stb_image/stb_image_impl.c
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#define STB_IMAGE_IMPLEMENTATION
|
||||||
|
#include "stb_image.h"
|
||||||
1724
vendors/stb_image/stb_image_write.h
vendored
Normal file
1724
vendors/stb_image/stb_image_write.h
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2
vendors/stb_image/stb_image_write_impl.c
vendored
Normal file
2
vendors/stb_image/stb_image_write_impl.c
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||||
|
#include "stb_image_write.h"
|
||||||
5079
vendors/stb_truetype/stb_truetype.h
vendored
Normal file
5079
vendors/stb_truetype/stb_truetype.h
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2
vendors/stb_truetype/stb_truetype_impl.c
vendored
Normal file
2
vendors/stb_truetype/stb_truetype_impl.c
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#define STB_TRUETYPE_IMPLEMENTATION
|
||||||
|
#include "stb_truetype.h"
|
||||||
Reference in New Issue
Block a user