Baseline: Ludic compiler + toolchain, Phase 1 syntax fixes complete
Self-hosted compiler (selfhost/*.ludic), runtime, examples, editor tooling, and docs. Phase 1 of the syntax-redesign cohesion pass has landed: edge-system fix, signature-query, when-alias, and the documentation truth-pass. Suite green (14/14), C-free bootstrap fixpoint holds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
985f9ad8f2
418 changed files with 39065 additions and 0 deletions
327
runtime/native/cocoa.ll
Normal file
327
runtime/native/cocoa.ll
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
; ============================================================================
|
||||
; cocoa.ll — the macOS window, written in LLVM IR.
|
||||
;
|
||||
; This is what replaced plat_cocoa.m. There is no Objective-C source and no C
|
||||
; source in a Ludic build: the window is opened by talking to the Objective-C
|
||||
; runtime through its C ABI (objc_getClass / sel_registerName / objc_msgSend)
|
||||
; and to Quartz through CoreGraphics, exactly as a compiled .m file would, only
|
||||
; written at the level ludicc itself emits. ludicc splices this module in when
|
||||
; it links a windowed macOS binary.
|
||||
;
|
||||
; The one piece that genuinely needs a class is the view: AppKit paints through
|
||||
; -drawRect:, so a class is built at runtime with objc_allocateClassPair and
|
||||
; @ludic_drawRect is installed as its IMP. On arm64 the unused NSRect argument
|
||||
; simply stays in registers we never read, so the IMP needs no struct in its
|
||||
; signature.
|
||||
;
|
||||
; Exposed to Ludic (through the win_* intrinsics):
|
||||
; win_open(w, h, scale, title) win_poll() -> key win_present(fb, w, h)
|
||||
; win_running() -> bool win_close()
|
||||
; ============================================================================
|
||||
|
||||
%CGRect = type { double, double, double, double }
|
||||
|
||||
declare ptr @objc_getClass(ptr)
|
||||
declare ptr @sel_registerName(ptr)
|
||||
declare ptr @objc_msgSend(ptr, ptr, ...)
|
||||
declare ptr @objc_allocateClassPair(ptr, ptr, i64)
|
||||
declare void @objc_registerClassPair(ptr)
|
||||
declare i8 @class_addMethod(ptr, ptr, ptr, ptr)
|
||||
declare ptr @CGColorSpaceCreateDeviceRGB()
|
||||
declare ptr @CGBitmapContextCreate(ptr, i64, i64, i64, i64, ptr, i32)
|
||||
declare ptr @CGBitmapContextCreateImage(ptr)
|
||||
declare void @CGContextDrawImage(ptr, %CGRect, ptr)
|
||||
declare void @CGContextSetInterpolationQuality(ptr, i32)
|
||||
declare void @CGContextTranslateCTM(ptr, double, double)
|
||||
declare void @CGContextScaleCTM(ptr, double, double)
|
||||
declare void @CGContextSaveGState(ptr)
|
||||
declare void @CGContextRestoreGState(ptr)
|
||||
declare void @CGImageRelease(ptr)
|
||||
declare void @CGContextRelease(ptr)
|
||||
declare void @CGColorSpaceRelease(ptr)
|
||||
declare i32 @usleep(i32)
|
||||
|
||||
@.c_app = private unnamed_addr constant [14 x i8] c"NSApplication\00"
|
||||
@.c_win = private unnamed_addr constant [9 x i8] c"NSWindow\00"
|
||||
@.c_str = private unnamed_addr constant [9 x i8] c"NSString\00"
|
||||
@.c_date = private unnamed_addr constant [7 x i8] c"NSDate\00"
|
||||
@.c_view = private unnamed_addr constant [7 x i8] c"NSView\00"
|
||||
@.c_gctx = private unnamed_addr constant [18 x i8] c"NSGraphicsContext\00"
|
||||
@.c_ludic = private unnamed_addr constant [10 x i8] c"LudicView\00"
|
||||
|
||||
@.s_shared = private unnamed_addr constant [18 x i8] c"sharedApplication\00"
|
||||
@.s_policy = private unnamed_addr constant [21 x i8] c"setActivationPolicy:\00"
|
||||
@.s_alloc = private unnamed_addr constant [6 x i8] c"alloc\00"
|
||||
@.s_initw = private unnamed_addr constant [45 x i8] c"initWithContentRect:styleMask:backing:defer:\00"
|
||||
@.s_initf = private unnamed_addr constant [15 x i8] c"initWithFrame:\00"
|
||||
@.s_title = private unnamed_addr constant [10 x i8] c"setTitle:\00"
|
||||
@.s_utf8 = private unnamed_addr constant [22 x i8] c"stringWithUTF8String:\00"
|
||||
@.s_setcv = private unnamed_addr constant [16 x i8] c"setContentView:\00"
|
||||
@.s_center = private unnamed_addr constant [7 x i8] c"center\00"
|
||||
@.s_mkey = private unnamed_addr constant [22 x i8] c"makeKeyAndOrderFront:\00"
|
||||
@.s_mfirst = private unnamed_addr constant [20 x i8] c"makeFirstResponder:\00"
|
||||
@.s_act = private unnamed_addr constant [27 x i8] c"activateIgnoringOtherApps:\00"
|
||||
@.s_next = private unnamed_addr constant [48 x i8] c"nextEventMatchingMask:untilDate:inMode:dequeue:\00"
|
||||
@.s_send = private unnamed_addr constant [11 x i8] c"sendEvent:\00"
|
||||
@.s_dpast = private unnamed_addr constant [12 x i8] c"distantPast\00"
|
||||
@.s_type = private unnamed_addr constant [5 x i8] c"type\00"
|
||||
@.s_keycd = private unnamed_addr constant [8 x i8] c"keyCode\00"
|
||||
@.s_chars = private unnamed_addr constant [28 x i8] c"charactersIgnoringModifiers\00"
|
||||
@.s_length = private unnamed_addr constant [7 x i8] c"length\00"
|
||||
@.s_charat = private unnamed_addr constant [18 x i8] c"characterAtIndex:\00"
|
||||
@.s_disp = private unnamed_addr constant [8 x i8] c"display\00"
|
||||
@.s_visib = private unnamed_addr constant [10 x i8] c"isVisible\00"
|
||||
@.s_curctx = private unnamed_addr constant [15 x i8] c"currentContext\00"
|
||||
@.s_cgctx = private unnamed_addr constant [10 x i8] c"CGContext\00"
|
||||
@.s_drawr = private unnamed_addr constant [10 x i8] c"drawRect:\00"
|
||||
@.s_afr = private unnamed_addr constant [23 x i8] c"acceptsFirstResponder\00\00"
|
||||
@.mode = private unnamed_addr constant [22 x i8] c"kCFRunLoopDefaultMode\00"
|
||||
@.enc_draw = private unnamed_addr constant [36 x i8] c"v@:{CGRect={CGPoint=dd}{CGSize=dd}}\00"
|
||||
@.enc_bool = private unnamed_addr constant [4 x i8] c"c@:\00"
|
||||
|
||||
@W_app = internal global ptr null
|
||||
@W_win = internal global ptr null
|
||||
@W_view = internal global ptr null
|
||||
@W_fb = internal global ptr null
|
||||
@W_fbw = internal global i32 320
|
||||
@W_fbh = internal global i32 240
|
||||
@W_scale = internal global i32 3
|
||||
@W_key = internal global i32 0
|
||||
@W_running = internal global i32 1
|
||||
|
||||
; -drawRect: — blit the framebuffer into the view.
|
||||
; The NSRect argument is ignored, so it never appears in this signature.
|
||||
define internal void @ludic_drawRect(ptr %self, ptr %sel) {
|
||||
entry:
|
||||
%fb = load ptr, ptr @W_fb
|
||||
%null = icmp eq ptr %fb, null
|
||||
br i1 %null, label %out, label %draw
|
||||
draw:
|
||||
%w32 = load i32, ptr @W_fbw
|
||||
%h32 = load i32, ptr @W_fbh
|
||||
%sc32 = load i32, ptr @W_scale
|
||||
%w = sext i32 %w32 to i64
|
||||
%h = sext i32 %h32 to i64
|
||||
%stride = mul i64 %w, 4
|
||||
|
||||
%cs = call ptr @CGColorSpaceCreateDeviceRGB()
|
||||
; kCGImageAlphaNoneSkipFirst (6) | kCGBitmapByteOrder32Little (8192)
|
||||
%bmp = call ptr @CGBitmapContextCreate(ptr %fb, i64 %w, i64 %h, i64 8, i64 %stride, ptr %cs, i32 8198)
|
||||
%img = call ptr @CGBitmapContextCreateImage(ptr %bmp)
|
||||
|
||||
%gcls = call ptr @objc_getClass(ptr @.c_gctx)
|
||||
%sel_cur = call ptr @sel_registerName(ptr @.s_curctx)
|
||||
%gctx = call ptr @objc_msgSend(ptr %gcls, ptr %sel_cur)
|
||||
%sel_cg = call ptr @sel_registerName(ptr @.s_cgctx)
|
||||
%ctx = call ptr @objc_msgSend(ptr %gctx, ptr %sel_cg)
|
||||
|
||||
call void @CGContextSetInterpolationQuality(ptr %ctx, i32 1) ; kCGInterpolationNone
|
||||
%scd = sitofp i32 %sc32 to double
|
||||
%wd = sitofp i32 %w32 to double
|
||||
%hd = sitofp i32 %h32 to double
|
||||
%dw = fmul double %wd, %scd
|
||||
%dh = fmul double %hd, %scd
|
||||
|
||||
; The framebuffer's row 0 is the top row, and a CGImage built over it carries
|
||||
; that orientation, so drawing it into the view's context lands right-side-up
|
||||
; with no CTM flip. (Adding one here mirrors the game vertically — verified by
|
||||
; drawing four different corners and looking at the window.)
|
||||
%r0 = insertvalue %CGRect undef, double 0.0, 0
|
||||
%r1 = insertvalue %CGRect %r0, double 0.0, 1
|
||||
%r2 = insertvalue %CGRect %r1, double %dw, 2
|
||||
%r3 = insertvalue %CGRect %r2, double %dh, 3
|
||||
call void @CGContextDrawImage(ptr %ctx, %CGRect %r3, ptr %img)
|
||||
|
||||
call void @CGImageRelease(ptr %img)
|
||||
call void @CGContextRelease(ptr %bmp)
|
||||
call void @CGColorSpaceRelease(ptr %cs)
|
||||
br label %out
|
||||
out:
|
||||
ret void
|
||||
}
|
||||
|
||||
; -acceptsFirstResponder — so the view receives key events
|
||||
define internal i8 @ludic_yes(ptr %self, ptr %sel) {
|
||||
entry:
|
||||
ret i8 1
|
||||
}
|
||||
|
||||
define void @win_open(i32 %w, i32 %h, i32 %scale, ptr %title) {
|
||||
entry:
|
||||
store i32 %w, ptr @W_fbw
|
||||
store i32 %h, ptr @W_fbh
|
||||
store i32 %scale, ptr @W_scale
|
||||
|
||||
%appcls = call ptr @objc_getClass(ptr @.c_app)
|
||||
%sel_sh = call ptr @sel_registerName(ptr @.s_shared)
|
||||
%app = call ptr @objc_msgSend(ptr %appcls, ptr %sel_sh)
|
||||
store ptr %app, ptr @W_app
|
||||
%sel_pol = call ptr @sel_registerName(ptr @.s_policy)
|
||||
%a1 = call ptr (ptr, ptr, i64) @objc_msgSend(ptr %app, ptr %sel_pol, i64 0)
|
||||
|
||||
; build the view class once: NSView + drawRect: + acceptsFirstResponder
|
||||
%viewsuper = call ptr @objc_getClass(ptr @.c_view)
|
||||
%cls = call ptr @objc_allocateClassPair(ptr %viewsuper, ptr @.c_ludic, i64 0)
|
||||
%sel_draw = call ptr @sel_registerName(ptr @.s_drawr)
|
||||
%m1 = call i8 @class_addMethod(ptr %cls, ptr %sel_draw, ptr @ludic_drawRect, ptr @.enc_draw)
|
||||
%sel_afr = call ptr @sel_registerName(ptr @.s_afr)
|
||||
%m2 = call i8 @class_addMethod(ptr %cls, ptr %sel_afr, ptr @ludic_yes, ptr @.enc_bool)
|
||||
call void @objc_registerClassPair(ptr %cls)
|
||||
|
||||
%wd = sitofp i32 %w to double
|
||||
%hd = sitofp i32 %h to double
|
||||
%scd = sitofp i32 %scale to double
|
||||
%pw = fmul double %wd, %scd
|
||||
%ph = fmul double %hd, %scd
|
||||
%r0 = insertvalue %CGRect undef, double 0.0, 0
|
||||
%r1 = insertvalue %CGRect %r0, double 0.0, 1
|
||||
%r2 = insertvalue %CGRect %r1, double %pw, 2
|
||||
%rect = insertvalue %CGRect %r2, double %ph, 3
|
||||
|
||||
%wincls = call ptr @objc_getClass(ptr @.c_win)
|
||||
%sel_alloc = call ptr @sel_registerName(ptr @.s_alloc)
|
||||
%w0 = call ptr @objc_msgSend(ptr %wincls, ptr %sel_alloc)
|
||||
%sel_initw = call ptr @sel_registerName(ptr @.s_initw)
|
||||
; NSWindowStyleMaskTitled|Closable = 3, NSBackingStoreBuffered = 2
|
||||
%win = call ptr (ptr, ptr, %CGRect, i64, i64, i8) @objc_msgSend(ptr %w0, ptr %sel_initw, %CGRect %rect, i64 3, i64 2, i8 0)
|
||||
store ptr %win, ptr @W_win
|
||||
|
||||
%strcls = call ptr @objc_getClass(ptr @.c_str)
|
||||
%sel_utf8 = call ptr @sel_registerName(ptr @.s_utf8)
|
||||
%nstitle = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %strcls, ptr %sel_utf8, ptr %title)
|
||||
%sel_title = call ptr @sel_registerName(ptr @.s_title)
|
||||
%a2 = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %win, ptr %sel_title, ptr %nstitle)
|
||||
|
||||
%v0 = call ptr @objc_msgSend(ptr %cls, ptr %sel_alloc)
|
||||
%sel_initf = call ptr @sel_registerName(ptr @.s_initf)
|
||||
%view = call ptr (ptr, ptr, %CGRect) @objc_msgSend(ptr %v0, ptr %sel_initf, %CGRect %rect)
|
||||
store ptr %view, ptr @W_view
|
||||
%sel_setcv = call ptr @sel_registerName(ptr @.s_setcv)
|
||||
%a3 = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %win, ptr %sel_setcv, ptr %view)
|
||||
%sel_center = call ptr @sel_registerName(ptr @.s_center)
|
||||
%a4 = call ptr @objc_msgSend(ptr %win, ptr %sel_center)
|
||||
%sel_mkey = call ptr @sel_registerName(ptr @.s_mkey)
|
||||
%a5 = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %win, ptr %sel_mkey, ptr null)
|
||||
%sel_mfirst = call ptr @sel_registerName(ptr @.s_mfirst)
|
||||
%a6 = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %win, ptr %sel_mfirst, ptr %view)
|
||||
%sel_act = call ptr @sel_registerName(ptr @.s_act)
|
||||
%a7 = call ptr (ptr, ptr, i8) @objc_msgSend(ptr %app, ptr %sel_act, i8 1)
|
||||
ret void
|
||||
}
|
||||
|
||||
; Drain the event queue, remembering the last key pressed. Arrow keys map onto
|
||||
; WASD and escape onto 'q', matching what the C backend did.
|
||||
define i32 @win_poll() {
|
||||
entry:
|
||||
store i32 0, ptr @W_key
|
||||
%app = load ptr, ptr @W_app
|
||||
%none = icmp eq ptr %app, null
|
||||
br i1 %none, label %done, label %setup
|
||||
setup:
|
||||
%sel_next = call ptr @sel_registerName(ptr @.s_next)
|
||||
%sel_send = call ptr @sel_registerName(ptr @.s_send)
|
||||
%sel_dp = call ptr @sel_registerName(ptr @.s_dpast)
|
||||
%sel_type = call ptr @sel_registerName(ptr @.s_type)
|
||||
%sel_kc = call ptr @sel_registerName(ptr @.s_keycd)
|
||||
%sel_ch = call ptr @sel_registerName(ptr @.s_chars)
|
||||
%sel_len = call ptr @sel_registerName(ptr @.s_length)
|
||||
%sel_cat = call ptr @sel_registerName(ptr @.s_charat)
|
||||
%sel_utf8 = call ptr @sel_registerName(ptr @.s_utf8)
|
||||
%datecls = call ptr @objc_getClass(ptr @.c_date)
|
||||
%strcls = call ptr @objc_getClass(ptr @.c_str)
|
||||
%mode = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %strcls, ptr %sel_utf8, ptr @.mode)
|
||||
br label %pump
|
||||
pump:
|
||||
%date = call ptr @objc_msgSend(ptr %datecls, ptr %sel_dp)
|
||||
%ev = call ptr (ptr, ptr, i64, ptr, ptr, i8) @objc_msgSend(ptr %app, ptr %sel_next, i64 -1, ptr %date, ptr %mode, i8 1)
|
||||
%got = icmp ne ptr %ev, null
|
||||
br i1 %got, label %handle, label %finish
|
||||
handle:
|
||||
%ty = call i64 (ptr, ptr) @objc_msgSend(ptr %ev, ptr %sel_type)
|
||||
%iskey = icmp eq i64 %ty, 10 ; NSEventTypeKeyDown
|
||||
br i1 %iskey, label %key, label %forward
|
||||
key:
|
||||
%kc = call i16 (ptr, ptr) @objc_msgSend(ptr %ev, ptr %sel_kc)
|
||||
%kc32 = zext i16 %kc to i32
|
||||
switch i32 %kc32, label %fromchars [
|
||||
i32 126, label %k_w
|
||||
i32 125, label %k_s
|
||||
i32 123, label %k_a
|
||||
i32 124, label %k_d
|
||||
i32 49, label %k_space
|
||||
i32 36, label %k_ret
|
||||
i32 53, label %k_esc
|
||||
]
|
||||
k_w: store i32 119, ptr @W_key br label %forward
|
||||
k_s: store i32 115, ptr @W_key br label %forward
|
||||
k_a: store i32 97, ptr @W_key br label %forward
|
||||
k_d: store i32 100, ptr @W_key br label %forward
|
||||
k_space: store i32 32, ptr @W_key br label %forward
|
||||
k_ret: store i32 10, ptr @W_key br label %forward
|
||||
k_esc:
|
||||
store i32 113, ptr @W_key
|
||||
store i32 0, ptr @W_running
|
||||
br label %forward
|
||||
fromchars:
|
||||
%chars = call ptr (ptr, ptr) @objc_msgSend(ptr %ev, ptr %sel_ch)
|
||||
%clen = call i64 (ptr, ptr) @objc_msgSend(ptr %chars, ptr %sel_len)
|
||||
%hasch = icmp sgt i64 %clen, 0
|
||||
br i1 %hasch, label %takechar, label %forward
|
||||
takechar:
|
||||
%ch = call i16 (ptr, ptr, i64) @objc_msgSend(ptr %chars, ptr %sel_cat, i64 0)
|
||||
%ch32 = zext i16 %ch to i32
|
||||
store i32 %ch32, ptr @W_key
|
||||
%isq = icmp eq i32 %ch32, 113
|
||||
br i1 %isq, label %quit, label %forward
|
||||
quit:
|
||||
store i32 0, ptr @W_running
|
||||
br label %forward
|
||||
forward:
|
||||
%fwd = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %app, ptr %sel_send, ptr %ev)
|
||||
br label %pump
|
||||
finish:
|
||||
; a closed window ends the run
|
||||
%win = load ptr, ptr @W_win
|
||||
%hw = icmp ne ptr %win, null
|
||||
br i1 %hw, label %checkvis, label %done
|
||||
checkvis:
|
||||
%sel_vis = call ptr @sel_registerName(ptr @.s_visib)
|
||||
%vis = call i8 (ptr, ptr) @objc_msgSend(ptr %win, ptr %sel_vis)
|
||||
%gone = icmp eq i8 %vis, 0
|
||||
br i1 %gone, label %closed, label %done
|
||||
closed:
|
||||
store i32 0, ptr @W_running
|
||||
br label %done
|
||||
done:
|
||||
%k = load i32, ptr @W_key
|
||||
ret i32 %k
|
||||
}
|
||||
|
||||
define void @win_present(ptr %fb, i32 %w, i32 %h) {
|
||||
entry:
|
||||
store ptr %fb, ptr @W_fb
|
||||
store i32 %w, ptr @W_fbw
|
||||
store i32 %h, ptr @W_fbh
|
||||
%view = load ptr, ptr @W_view
|
||||
%none = icmp eq ptr %view, null
|
||||
br i1 %none, label %out, label %show
|
||||
show:
|
||||
%sel_disp = call ptr @sel_registerName(ptr @.s_disp)
|
||||
%d = call ptr @objc_msgSend(ptr %view, ptr %sel_disp)
|
||||
%u = call i32 @usleep(i32 16000) ; ~60 fps
|
||||
br label %out
|
||||
out:
|
||||
ret void
|
||||
}
|
||||
|
||||
define i32 @win_running() {
|
||||
entry:
|
||||
%r = load i32, ptr @W_running
|
||||
ret i32 %r
|
||||
}
|
||||
|
||||
define void @win_close() {
|
||||
entry:
|
||||
store i32 0, ptr @W_running
|
||||
ret void
|
||||
}
|
||||
394
runtime/native/core.ludic
Normal file
394
runtime/native/core.ludic
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
# ============================================================================
|
||||
# runtime/native/core.ludic — the Ludic runtime, written in Ludic.
|
||||
#
|
||||
# This is what the native backend links instead of a C runtime. Everything a
|
||||
# game touches through a builtin — the framebuffer, rectangles, text, the
|
||||
# registers, the RNG, input, the frame dump — is implemented right here in
|
||||
# Ludic and compiled to machine code by ludicc like any other Ludic source.
|
||||
#
|
||||
# Underneath it there is exactly one layer, and it is not C: the compiler
|
||||
# intrinsics (mem_alloc, peek8/poke8, peek32/poke32, file_open/file_write,
|
||||
# read_byte, …) lower to direct calls into the platform's C ABI — malloc,
|
||||
# fwrite, getchar. That ABI is the operating system's interface, the same floor
|
||||
# Rust and Swift stand on. No C source is compiled at any point in the build.
|
||||
#
|
||||
# A builtin `clear(c)` in a game resolves to `rt_clear(c)` here; that is the
|
||||
# whole protocol. Swap this file and you have swapped the runtime.
|
||||
#
|
||||
# This file is a fragment: ludicc splices it into every native build. It is not
|
||||
# a `game`/`module` block of its own.
|
||||
# ============================================================================
|
||||
|
||||
# ---- state ----------------------------------------------------------------
|
||||
var rt_fb: ptr = ptr_null() # framebuffer, one i32 (0x00RRGGBB) per pixel
|
||||
var rt_fbw: int = 320
|
||||
var rt_fbh: int = 240
|
||||
var rt_regs: ptr = ptr_null() # the 64 general-purpose game registers
|
||||
var rt_rng: int = 305419896 # xorshift32 state
|
||||
var rt_alive: int = 1 # platform still running?
|
||||
|
||||
# 5x7 glyphs for ASCII 32..90, 7 rows per glyph, each row a 5-bit mask stored
|
||||
# biased by '0' so the whole font is one printable string literal.
|
||||
fn rt_font() -> str {
|
||||
return "00000004444404000000000000000000000IJ4:FC000000000000000000000000000000E>O>E0044O4400000448000O000000004012448@@>ACEIA>4<4444>>A168@ON11>11N26:BO22O@N11A>>@@NAA>O124888>AA>AA>>AA?11>04004000000000024842000O0O000842480>A164040000000>AAOAAANAANAAN>A@@@A>LBAAABLO@@L@@OO@@L@@@>A@GAA>AAAOAAA>44444>7222BB<ABDHDBA@@@@@@OAKEEAAAAIEECAA>AAAAA>NAAN@@@>AAAEB=NAANDBA>A@>1A>O444444AAAAAA>AAAAA:4AAAEEKAAA:4:AAAA:4444O1248@O"
|
||||
}
|
||||
|
||||
fn rt_init() -> void {
|
||||
rt_fb = mem_alloc(320 * 240 * 4)
|
||||
rt_regs = mem_alloc(64 * 4)
|
||||
mem_set(rt_regs, 0, 64 * 4)
|
||||
rt_map = mem_alloc(96 * 64)
|
||||
mem_set(rt_map, 32, 96 * 64)
|
||||
rt_statusbuf = mem_alloc(96)
|
||||
poke8(rt_statusbuf, 0, 0)
|
||||
rt_image_init()
|
||||
rt_tt_init()
|
||||
rt_ui_init()
|
||||
rt_clear(0)
|
||||
if is_windowed() {
|
||||
win_open(rt_fbw, rt_fbh, 3, game_title())
|
||||
}
|
||||
}
|
||||
|
||||
fn rt_shutdown() -> void {
|
||||
if is_windowed() {
|
||||
win_close()
|
||||
return
|
||||
}
|
||||
rt_dump_ppm("out.ppm")
|
||||
}
|
||||
|
||||
# ---- framebuffer ----------------------------------------------------------
|
||||
fn rt_clear(c: int) -> void {
|
||||
let n = rt_fbw * rt_fbh
|
||||
for i in 0 .. n {
|
||||
poke32(rt_fb, i, c)
|
||||
}
|
||||
}
|
||||
|
||||
fn rt_put_px(x: int, y: int, c: int) -> void {
|
||||
if x < 0 { return }
|
||||
if y < 0 { return }
|
||||
if x >= rt_fbw { return }
|
||||
if y >= rt_fbh { return }
|
||||
poke32(rt_fb, y * rt_fbw + x, c)
|
||||
}
|
||||
|
||||
fn rt_fill_rect(x: int, y: int, w: int, h: int, c: int) -> void {
|
||||
let x0 = max(0, x)
|
||||
let y0 = max(0, y)
|
||||
let x1 = min(rt_fbw, x + w)
|
||||
let y1 = min(rt_fbh, y + h)
|
||||
let j = y0
|
||||
while j < y1 {
|
||||
let row = j * rt_fbw
|
||||
let i = x0
|
||||
while i < x1 {
|
||||
poke32(rt_fb, row + i, c)
|
||||
i = i + 1
|
||||
}
|
||||
j = j + 1
|
||||
}
|
||||
}
|
||||
|
||||
fn rt_frame_rect(x: int, y: int, w: int, h: int, c: int) -> void {
|
||||
rt_fill_rect(x, y, w, 1, c)
|
||||
rt_fill_rect(x, y + h - 1, w, 1, c)
|
||||
rt_fill_rect(x, y, 1, h, c)
|
||||
rt_fill_rect(x + w - 1, y, 1, h, c)
|
||||
}
|
||||
|
||||
# Windowed: hand the framebuffer to the platform layer, which blits it into
|
||||
# the view. Headless: nothing to do until shutdown writes the last frame out.
|
||||
fn rt_present() -> void {
|
||||
if is_windowed() {
|
||||
win_present(rt_fb, rt_fbw, rt_fbh)
|
||||
}
|
||||
}
|
||||
|
||||
# ---- text -----------------------------------------------------------------
|
||||
fn rt_glyph(x: int, y: int, ch: int, colour: int, sc: int) -> void {
|
||||
let c = ch
|
||||
if c >= 97 {
|
||||
if c <= 122 { c = c - 32 }
|
||||
}
|
||||
if c < 32 { return }
|
||||
if c > 90 { return }
|
||||
let base = (c - 32) * 7
|
||||
let font = rt_font()
|
||||
for row in 0 .. 7 {
|
||||
let bits = peek8(font, base + row) - 48
|
||||
let b = bits
|
||||
for cc in 0 .. 5 {
|
||||
let on = b / 16
|
||||
if on == 1 {
|
||||
rt_fill_rect(x + cc * sc, y + row * sc, sc, sc, colour)
|
||||
}
|
||||
b = (b - on * 16) * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rt_text(x: int, y: int, s: str, colour: int, sc: int) -> void {
|
||||
let i = 0
|
||||
let cx = x
|
||||
let ch = peek8(s, 0)
|
||||
while ch != 0 {
|
||||
rt_glyph(cx, y, ch, colour, sc)
|
||||
cx = cx + 6 * sc
|
||||
i = i + 1
|
||||
ch = peek8(s, i)
|
||||
}
|
||||
}
|
||||
|
||||
fn rt_text_int(x: int, y: int, n: int, colour: int, sc: int) -> void {
|
||||
if n == 0 {
|
||||
rt_glyph(x, y, 48, colour, sc)
|
||||
return
|
||||
}
|
||||
let v = n
|
||||
let cx = x
|
||||
if v < 0 {
|
||||
rt_glyph(cx, y, 45, colour, sc)
|
||||
cx = cx + 6 * sc
|
||||
v = 0 - v
|
||||
}
|
||||
let digits = 0
|
||||
let t = v
|
||||
while t > 0 {
|
||||
digits = digits + 1
|
||||
t = t / 10
|
||||
}
|
||||
let p = digits
|
||||
while p > 0 {
|
||||
let div = 1
|
||||
for k in 1 .. p {
|
||||
div = div * 10
|
||||
}
|
||||
rt_glyph(cx, y, 48 + (v / div) % 10, colour, sc)
|
||||
cx = cx + 6 * sc
|
||||
p = p - 1
|
||||
}
|
||||
}
|
||||
|
||||
# ---- registers ------------------------------------------------------------
|
||||
fn rt_reg(i: int) -> int {
|
||||
if i < 0 { return 0 }
|
||||
if i >= 64 { return 0 }
|
||||
return peek32(rt_regs, i)
|
||||
}
|
||||
|
||||
fn rt_setreg(i: int, v: int) -> void {
|
||||
if i < 0 { return }
|
||||
if i >= 64 { return }
|
||||
poke32(rt_regs, i, v)
|
||||
}
|
||||
|
||||
# ---- rng (xorshift32) -----------------------------------------------------
|
||||
fn rt_seed(s: int) -> void {
|
||||
if s == 0 {
|
||||
rt_rng = 305419896
|
||||
return
|
||||
}
|
||||
rt_rng = s
|
||||
}
|
||||
|
||||
# xorshift32 (Marsaglia). Runs on the raw 32-bit pattern, so the sign bit is
|
||||
# masked off only when a caller asks for a number.
|
||||
fn rt_next_rand() -> int {
|
||||
let x = rt_rng
|
||||
x = bxor(x, shl(x, 13))
|
||||
x = bxor(x, shr(x, 17))
|
||||
x = bxor(x, shl(x, 5))
|
||||
rt_rng = x
|
||||
return band(x, 2147483647)
|
||||
}
|
||||
|
||||
fn rt_rng_range(lo: int, hi: int) -> int {
|
||||
if hi <= lo { return lo }
|
||||
return lo + rt_next_rand() % (hi - lo + 1)
|
||||
}
|
||||
|
||||
fn rt_rng_chance(pct: int) -> bool {
|
||||
return rt_next_rand() % 100 < pct
|
||||
}
|
||||
|
||||
# ---- platform: input ------------------------------------------------------
|
||||
fn rt_poll() -> int {
|
||||
if is_windowed() {
|
||||
return win_poll()
|
||||
}
|
||||
let c = read_byte()
|
||||
if c < 0 {
|
||||
rt_alive = 0
|
||||
return 0
|
||||
}
|
||||
if c == 113 { # 'q' quits, as in the headless C platform
|
||||
rt_alive = 0
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
fn rt_running() -> bool {
|
||||
if is_windowed() {
|
||||
return win_running()
|
||||
}
|
||||
return rt_alive
|
||||
}
|
||||
|
||||
# ---- writing the frame out ------------------------------------------------
|
||||
fn rt_put_str(buf: ptr, at: int, s: str) -> int {
|
||||
let i = 0
|
||||
let n = at
|
||||
let ch = peek8(s, 0)
|
||||
while ch != 0 {
|
||||
poke8(buf, n, ch)
|
||||
n = n + 1
|
||||
i = i + 1
|
||||
ch = peek8(s, i)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
fn rt_put_int(buf: ptr, at: int, v: int) -> int {
|
||||
if v == 0 {
|
||||
poke8(buf, at, 48)
|
||||
return at + 1
|
||||
}
|
||||
let digits = 0
|
||||
let t = v
|
||||
while t > 0 {
|
||||
digits = digits + 1
|
||||
t = t / 10
|
||||
}
|
||||
let n = at
|
||||
let p = digits
|
||||
while p > 0 {
|
||||
let div = 1
|
||||
for k in 1 .. p {
|
||||
div = div * 10
|
||||
}
|
||||
poke8(buf, n, 48 + (v / div) % 10)
|
||||
n = n + 1
|
||||
p = p - 1
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
fn rt_dump_ppm(path: str) -> void {
|
||||
let f = file_open(path, "wb")
|
||||
if ptr_is_null(f) { return }
|
||||
|
||||
let hdr = mem_alloc(64)
|
||||
let n = rt_put_str(hdr, 0, "P6\n")
|
||||
n = rt_put_int(hdr, n, rt_fbw)
|
||||
n = rt_put_str(hdr, n, " ")
|
||||
n = rt_put_int(hdr, n, rt_fbh)
|
||||
n = rt_put_str(hdr, n, "\n255\n")
|
||||
file_write(f, hdr, n)
|
||||
|
||||
let px = rt_fbw * rt_fbh
|
||||
let buf = mem_alloc(px * 3)
|
||||
for i in 0 .. px {
|
||||
let c = peek32(rt_fb, i)
|
||||
poke8(buf, i * 3, (c / 65536) % 256)
|
||||
poke8(buf, i * 3 + 1, (c / 256) % 256)
|
||||
poke8(buf, i * 3 + 2, c % 256)
|
||||
}
|
||||
file_write(f, buf, px * 3)
|
||||
file_close(f)
|
||||
mem_free(buf)
|
||||
mem_free(hdr)
|
||||
}
|
||||
|
||||
import "inflate.ludic"
|
||||
import "image.ludic"
|
||||
import "truetype.ludic"
|
||||
import "ui.ludic"
|
||||
|
||||
# ---- tilemap --------------------------------------------------------------
|
||||
# A character grid the game paints with map_row() and reads with tile(). Stored
|
||||
# as one byte per cell in a flat buffer; out-of-bounds reads answer '#' so a
|
||||
# caller can treat the edge of the world as wall without special-casing it.
|
||||
var rt_map: ptr = ptr_null()
|
||||
var rt_mapw: int = 0
|
||||
var rt_maph: int = 0
|
||||
|
||||
fn rt_map_size(w: int, h: int) -> void {
|
||||
rt_mapw = clamp(w, 0, 96)
|
||||
rt_maph = clamp(h, 0, 64)
|
||||
mem_set(rt_map, 32, 96 * 64)
|
||||
}
|
||||
|
||||
fn rt_map_row(y: int, s: str) -> void {
|
||||
if y < 0 { return }
|
||||
if y >= 64 { return }
|
||||
let x = 0
|
||||
let ch = peek8(s, 0)
|
||||
while ch != 0 {
|
||||
if x >= 96 { return }
|
||||
poke8(rt_map, y * 96 + x, ch)
|
||||
x = x + 1
|
||||
ch = peek8(s, x)
|
||||
}
|
||||
}
|
||||
|
||||
fn rt_tile(x: int, y: int) -> int {
|
||||
if x < 0 { return 35 }
|
||||
if y < 0 { return 35 }
|
||||
if x >= rt_mapw { return 35 }
|
||||
if y >= rt_maph { return 35 }
|
||||
return peek8(rt_map, y * 96 + x)
|
||||
}
|
||||
|
||||
# ---- status line ----------------------------------------------------------
|
||||
# One persistent string of feedback/dialogue, copied into runtime-owned memory
|
||||
# so it survives whatever the caller does with the original.
|
||||
var rt_statusbuf: ptr = ptr_null()
|
||||
|
||||
fn rt_status(s: str) -> void {
|
||||
let i = 0
|
||||
let ch = peek8(s, 0)
|
||||
while ch != 0 {
|
||||
if i >= 95 { ch = 0 }
|
||||
if ch != 0 {
|
||||
poke8(rt_statusbuf, i, ch)
|
||||
i = i + 1
|
||||
ch = peek8(s, i)
|
||||
}
|
||||
}
|
||||
poke8(rt_statusbuf, i, 0)
|
||||
}
|
||||
|
||||
fn rt_status_text() -> ptr {
|
||||
return rt_statusbuf
|
||||
}
|
||||
|
||||
# ---- snapshot: the runtime serialises its own half ------------------------
|
||||
# The compiler writes the ECS (entities, components, archetype kinds) because
|
||||
# only it knows their shape. Everything below belongs to the runtime, so the
|
||||
# runtime writes it — same order both ways.
|
||||
fn rt_save_state(f: ptr) -> void {
|
||||
let w = mem_alloc(16)
|
||||
poke32(w, 0, rt_rng)
|
||||
poke32(w, 1, rt_mapw)
|
||||
poke32(w, 2, rt_maph)
|
||||
poke32(w, 3, rt_alive)
|
||||
file_write(f, w, 16)
|
||||
file_write(f, rt_regs, 64 * 4)
|
||||
file_write(f, rt_map, 96 * 64)
|
||||
file_write(f, rt_statusbuf, 96)
|
||||
mem_free(w)
|
||||
}
|
||||
|
||||
fn rt_load_state(f: ptr) -> void {
|
||||
let w = mem_alloc(16)
|
||||
file_read(f, w, 16)
|
||||
rt_rng = peek32(w, 0)
|
||||
rt_mapw = peek32(w, 1)
|
||||
rt_maph = peek32(w, 2)
|
||||
file_read(f, rt_regs, 64 * 4)
|
||||
file_read(f, rt_map, 96 * 64)
|
||||
file_read(f, rt_statusbuf, 96)
|
||||
mem_free(w)
|
||||
}
|
||||
436
runtime/native/image.ludic
Normal file
436
runtime/native/image.ludic
Normal file
|
|
@ -0,0 +1,436 @@
|
|||
# ============================================================================
|
||||
# image.ludic — PNG decoding, images and sprites, written in Ludic.
|
||||
#
|
||||
# Replaces the C runtime's image half. Reads a real .png off disk, inflates the
|
||||
# IDAT stream with the Ludic DEFLATE decoder next door, reverses the per-scanline
|
||||
# filters, and expands whatever colour type the file uses into 0xAARRGGBB.
|
||||
#
|
||||
# Supported: 8/4/2/1-bit greyscale, 8-bit truecolour, indexed with PLTE/tRNS,
|
||||
# greyscale+alpha and RGBA — the same set the C runtime handled.
|
||||
# ============================================================================
|
||||
|
||||
const IMG_MAX: int = 32
|
||||
const SPR_MAX: int = 160
|
||||
const SPR_SZ: int = 16
|
||||
|
||||
var img_px: ptr = ptr_null() # IMG_MAX pointers to RGBA buffers
|
||||
var img_w: ptr = ptr_null()
|
||||
var img_h: ptr = ptr_null()
|
||||
var img_n: int = 0
|
||||
|
||||
var spr_px: ptr = ptr_null() # SPR_MAX * 16 * 16 RGBA pixels, one block
|
||||
var spr_n: int = 0
|
||||
|
||||
fn rt_image_init() -> void {
|
||||
img_px = mem_alloc(IMG_MAX * 8)
|
||||
img_w = mem_alloc(IMG_MAX * 4)
|
||||
img_h = mem_alloc(IMG_MAX * 4)
|
||||
spr_px = mem_alloc(SPR_MAX * SPR_SZ * SPR_SZ * 4)
|
||||
mem_set(spr_px, 0, SPR_MAX * SPR_SZ * SPR_SZ * 4)
|
||||
}
|
||||
|
||||
# ---- decoding -------------------------------------------------------------
|
||||
fn png_be32(b: ptr, at: int) -> int {
|
||||
return bor(bor(shl(peek8(b, at), 24), shl(peek8(b, at + 1), 16)),
|
||||
bor(shl(peek8(b, at + 2), 8), peek8(b, at + 3)))
|
||||
}
|
||||
|
||||
fn png_tag(b: ptr, at: int, a: int, c: int, d: int, e: int) -> bool {
|
||||
if peek8(b, at) != a { return false }
|
||||
if peek8(b, at + 1) != c { return false }
|
||||
if peek8(b, at + 2) != d { return false }
|
||||
if peek8(b, at + 3) != e { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
fn rt_read_file(path: str) -> ptr {
|
||||
let f = file_open(path, "rb")
|
||||
if ptr_is_null(f) { return ptr_null() }
|
||||
file_seek(f, 0, 2)
|
||||
let n = file_tell(f)
|
||||
file_seek(f, 0, 0)
|
||||
if n <= 0 { file_close(f) return ptr_null() }
|
||||
let buf = mem_alloc(n + 8)
|
||||
file_read(f, buf, n)
|
||||
file_close(f)
|
||||
rt_file_len = n
|
||||
return buf
|
||||
}
|
||||
var rt_file_len: int = 0
|
||||
|
||||
# decoded image is left in these; -1 width means failure
|
||||
var png_w: int = 0
|
||||
var png_h: int = 0
|
||||
var png_px: ptr = ptr_null()
|
||||
|
||||
fn rt_decode_png(path: str) -> bool {
|
||||
png_w = 0
|
||||
png_h = 0
|
||||
png_px = ptr_null()
|
||||
let d = rt_read_file(path)
|
||||
if ptr_is_null(d) { return false }
|
||||
let size = rt_file_len
|
||||
if size < 8 { mem_free(d) return false }
|
||||
if peek8(d, 0) != 137 { mem_free(d) return false }
|
||||
if peek8(d, 1) != 80 { mem_free(d) return false }
|
||||
|
||||
let w = 0
|
||||
let h = 0
|
||||
let bd = 0
|
||||
let ct = 0
|
||||
let plte = mem_alloc(768)
|
||||
let trns = mem_alloc(256)
|
||||
let ntrns = 0
|
||||
let idat = mem_alloc(size)
|
||||
let idlen = 0
|
||||
let i = 8
|
||||
let done = 0
|
||||
|
||||
while done == 0 {
|
||||
if i + 8 > size { done = 1 }
|
||||
if done == 0 {
|
||||
let ln = png_be32(d, i)
|
||||
let typ = i + 4
|
||||
let body = i + 8
|
||||
if ln < 0 { done = 1 }
|
||||
if body + ln > size { done = 1 }
|
||||
if done == 0 {
|
||||
if png_tag(d, typ, 73, 72, 68, 82) { # IHDR
|
||||
w = png_be32(d, body)
|
||||
h = png_be32(d, body + 4)
|
||||
bd = peek8(d, body + 8)
|
||||
ct = peek8(d, body + 9)
|
||||
}
|
||||
if png_tag(d, typ, 80, 76, 84, 69) { # PLTE
|
||||
let m = min(ln, 768)
|
||||
for k in 0 .. m { poke8(plte, k, peek8(d, body + k)) }
|
||||
}
|
||||
if png_tag(d, typ, 116, 82, 78, 83) { # tRNS
|
||||
ntrns = min(ln, 256)
|
||||
for k in 0 .. ntrns { poke8(trns, k, peek8(d, body + k)) }
|
||||
}
|
||||
if png_tag(d, typ, 73, 68, 65, 84) { # IDAT
|
||||
for k in 0 .. ln { poke8(idat, idlen + k, peek8(d, body + k)) }
|
||||
idlen = idlen + ln
|
||||
}
|
||||
if png_tag(d, typ, 73, 69, 78, 68) { done = 1 } # IEND
|
||||
i = i + 12 + ln
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if w <= 0 { mem_free(d) return false }
|
||||
if h <= 0 { mem_free(d) return false }
|
||||
|
||||
let channels = 1
|
||||
if ct == 2 { channels = 3 }
|
||||
if ct == 6 { channels = 4 }
|
||||
if ct == 4 { channels = 2 }
|
||||
let bppbits = bd * channels
|
||||
let fbpp = (bppbits + 7) / 8
|
||||
if fbpp < 1 { fbpp = 1 }
|
||||
let stride = (w * bppbits + 7) / 8
|
||||
let rawlen = h * (stride + 1)
|
||||
let raw = mem_alloc(rawlen + 8)
|
||||
if z_uncompress(idat, idlen, raw, rawlen) < 0 {
|
||||
mem_free(d) mem_free(raw) mem_free(idat)
|
||||
return false
|
||||
}
|
||||
|
||||
# reverse the per-scanline filters, in place
|
||||
for y in 0 .. h {
|
||||
let line = y * (stride + 1)
|
||||
let ft = peek8(raw, line)
|
||||
let cur = line + 1
|
||||
let prev = cur - (stride + 1)
|
||||
for x in 0 .. stride {
|
||||
let a = 0
|
||||
let b = 0
|
||||
let c = 0
|
||||
if x >= fbpp { a = peek8(raw, cur + x - fbpp) }
|
||||
if y > 0 { b = peek8(raw, prev + x) }
|
||||
if x >= fbpp { if y > 0 { c = peek8(raw, prev + x - fbpp) } }
|
||||
let v = peek8(raw, cur + x)
|
||||
if ft == 1 { v = v + a }
|
||||
if ft == 2 { v = v + b }
|
||||
if ft == 3 { v = v + (a + b) / 2 }
|
||||
if ft == 4 {
|
||||
let p = a + b - c
|
||||
let pa = abs(p - a)
|
||||
let pb = abs(p - b)
|
||||
let pc = abs(p - c)
|
||||
let pick = c
|
||||
if pb <= pc { pick = b }
|
||||
if pa <= pb { if pa <= pc { pick = a } }
|
||||
v = v + pick
|
||||
}
|
||||
poke8(raw, cur + x, band(v, 255))
|
||||
}
|
||||
}
|
||||
|
||||
# expand to 0xAARRGGBB
|
||||
let out = mem_alloc(w * h * 4)
|
||||
let maxv = shl(1, bd) - 1
|
||||
for y in 0 .. h {
|
||||
let row = y * (stride + 1) + 1
|
||||
for x in 0 .. w {
|
||||
let R = 0
|
||||
let G = 0
|
||||
let B = 0
|
||||
let A = 255
|
||||
if ct == 6 {
|
||||
R = peek8(raw, row + x * 4)
|
||||
G = peek8(raw, row + x * 4 + 1)
|
||||
B = peek8(raw, row + x * 4 + 2)
|
||||
A = peek8(raw, row + x * 4 + 3)
|
||||
}
|
||||
if ct == 2 {
|
||||
R = peek8(raw, row + x * 3)
|
||||
G = peek8(raw, row + x * 3 + 1)
|
||||
B = peek8(raw, row + x * 3 + 2)
|
||||
}
|
||||
if ct == 4 {
|
||||
R = peek8(raw, row + x * 2)
|
||||
G = R
|
||||
B = R
|
||||
A = peek8(raw, row + x * 2 + 1)
|
||||
}
|
||||
if ct == 3 {
|
||||
let bp = x * bd
|
||||
let idx = band(shr(peek8(raw, row + bp / 8), 8 - bd - bp % 8), maxv)
|
||||
R = peek8(plte, idx * 3)
|
||||
G = peek8(plte, idx * 3 + 1)
|
||||
B = peek8(plte, idx * 3 + 2)
|
||||
if idx < ntrns { A = peek8(trns, idx) }
|
||||
}
|
||||
if ct == 0 {
|
||||
let bp = x * bd
|
||||
let s = band(shr(peek8(raw, row + bp / 8), 8 - bd - bp % 8), maxv)
|
||||
let g = s
|
||||
if bd != 8 { g = s * 255 / maxv }
|
||||
R = g
|
||||
G = g
|
||||
B = g
|
||||
}
|
||||
poke32(out, y * w + x, bor(bor(shl(A, 24), shl(R, 16)), bor(shl(G, 8), B)))
|
||||
}
|
||||
}
|
||||
|
||||
mem_free(d)
|
||||
mem_free(idat)
|
||||
mem_free(raw)
|
||||
mem_free(plte)
|
||||
mem_free(trns)
|
||||
png_w = w
|
||||
png_h = h
|
||||
png_px = out
|
||||
return true
|
||||
}
|
||||
|
||||
# ---- images ---------------------------------------------------------------
|
||||
fn rt_image_load(path: str) -> int {
|
||||
if img_n >= IMG_MAX { return 0 - 1 }
|
||||
if rt_decode_png(path) == false { return 0 - 1 }
|
||||
let id = img_n
|
||||
img_n = img_n + 1
|
||||
pokep(img_px, id, png_px)
|
||||
poke32(img_w, id, png_w)
|
||||
poke32(img_h, id, png_h)
|
||||
return id
|
||||
}
|
||||
|
||||
fn rt_blend_px(x: int, y: int, argb: int) -> void {
|
||||
let a = band(shr(argb, 24), 255)
|
||||
if a == 0 { return }
|
||||
if x < 0 { return }
|
||||
if y < 0 { return }
|
||||
if x >= rt_fbw { return }
|
||||
if y >= rt_fbh { return }
|
||||
let r = band(shr(argb, 16), 255)
|
||||
let g = band(shr(argb, 8), 255)
|
||||
let b = band(argb, 255)
|
||||
if a != 255 {
|
||||
let dst = peek32(rt_fb, y * rt_fbw + x)
|
||||
let dr = band(shr(dst, 16), 255)
|
||||
let dg = band(shr(dst, 8), 255)
|
||||
let db = band(dst, 255)
|
||||
r = (r * a + dr * (255 - a)) / 255
|
||||
g = (g * a + dg * (255 - a)) / 255
|
||||
b = (b * a + db * (255 - a)) / 255
|
||||
}
|
||||
poke32(rt_fb, y * rt_fbw + x, bor(bor(shl(r, 16), shl(g, 8)), b))
|
||||
}
|
||||
|
||||
fn rt_draw_image(id: int, dx: int, dy: int) -> void {
|
||||
if id < 0 { return }
|
||||
if id >= img_n { return }
|
||||
let w = peek32(img_w, id)
|
||||
let h = peek32(img_h, id)
|
||||
let s = peekp(img_px, id)
|
||||
for y in 0 .. h {
|
||||
for x in 0 .. w {
|
||||
rt_blend_px(dx + x, dy + y, peek32(s, y * w + x))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rt_draw_image_scaled(id: int, dx: int, dy: int, dw: int, dh: int) -> void {
|
||||
if id < 0 { return }
|
||||
if id >= img_n { return }
|
||||
if dw <= 0 { return }
|
||||
if dh <= 0 { return }
|
||||
let w = peek32(img_w, id)
|
||||
let h = peek32(img_h, id)
|
||||
let s = peekp(img_px, id)
|
||||
for y in 0 .. dh {
|
||||
for x in 0 .. dw {
|
||||
rt_blend_px(dx + x, dy + y, peek32(s, (y * h / dh) * w + x * w / dw))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# map one destination axis onto the source for a 9-slice: the two insets are
|
||||
# copied 1:1 and only the middle stretches
|
||||
fn rt_9map(d: int, dsz: int, ssz: int, inset: int) -> int {
|
||||
if inset * 2 >= dsz { return d * ssz / max(dsz, 1) }
|
||||
if inset * 2 >= ssz { return d * ssz / max(dsz, 1) }
|
||||
if d < inset { return d }
|
||||
if d >= dsz - inset { return ssz - (dsz - d) }
|
||||
return inset + (d - inset) * (ssz - 2 * inset) / (dsz - 2 * inset)
|
||||
}
|
||||
|
||||
fn rt_draw_9slice(id: int, dx: int, dy: int, dw: int, dh: int, inset: int) -> void {
|
||||
if id < 0 { return }
|
||||
if id >= img_n { return }
|
||||
if dw <= 0 { return }
|
||||
if dh <= 0 { return }
|
||||
let w = peek32(img_w, id)
|
||||
let h = peek32(img_h, id)
|
||||
let s = peekp(img_px, id)
|
||||
for y in 0 .. dh {
|
||||
let sy = rt_9map(y, dh, h, inset)
|
||||
for x in 0 .. dw {
|
||||
rt_blend_px(dx + x, dy + y, peek32(s, sy * w + rt_9map(x, dw, w, inset)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---- sprites (16x16 art) --------------------------------------------------
|
||||
fn rt_load_png(path: str) -> int {
|
||||
if spr_n >= SPR_MAX { return 0 - 1 }
|
||||
if rt_decode_png(path) == false { return 0 - 1 }
|
||||
let id = spr_n
|
||||
spr_n = spr_n + 1
|
||||
let base = id * SPR_SZ * SPR_SZ
|
||||
for y in 0 .. SPR_SZ {
|
||||
for x in 0 .. SPR_SZ {
|
||||
let px = 0
|
||||
if x < png_w {
|
||||
if y < png_h {
|
||||
let c = peek32(png_px, y * png_w + x)
|
||||
if band(shr(c, 24), 255) >= 128 { px = bor(shl(255, 24), band(c, 16777215)) }
|
||||
}
|
||||
}
|
||||
poke32(spr_px, base + y * SPR_SZ + x, px)
|
||||
}
|
||||
}
|
||||
mem_free(png_px)
|
||||
png_px = ptr_null()
|
||||
return id
|
||||
}
|
||||
|
||||
fn rt_draw_sprite(id: int, px: int, py: int) -> void {
|
||||
if id < 0 { return }
|
||||
if id >= spr_n { return }
|
||||
let base = id * SPR_SZ * SPR_SZ
|
||||
for y in 0 .. SPR_SZ {
|
||||
for x in 0 .. SPR_SZ {
|
||||
let p = peek32(spr_px, base + y * SPR_SZ + x)
|
||||
if band(shr(p, 24), 255) != 0 {
|
||||
rt_put_px(px + x, py + y, band(p, 16777215))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rt_draw_sprite_scaled(id: int, px: int, py: int, sc: int) -> void {
|
||||
if id < 0 { return }
|
||||
if id >= spr_n { return }
|
||||
if sc < 1 { return }
|
||||
let base = id * SPR_SZ * SPR_SZ
|
||||
for y in 0 .. SPR_SZ {
|
||||
for x in 0 .. SPR_SZ {
|
||||
let p = peek32(spr_px, base + y * SPR_SZ + x)
|
||||
if band(shr(p, 24), 255) != 0 {
|
||||
rt_fill_rect(px + x * sc, py + y * sc, sc, sc, band(p, 16777215))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---- .lspr sprite sheets (palette + ASCII rows) ---------------------------
|
||||
fn rt_hexval(c: int) -> int {
|
||||
if c >= 48 { if c <= 57 { return c - 48 } }
|
||||
if c >= 97 { if c <= 102 { return c - 87 } }
|
||||
if c >= 65 { if c <= 70 { return c - 55 } }
|
||||
return 0 - 1
|
||||
}
|
||||
|
||||
fn rt_load_sprites(path: str) -> void {
|
||||
let d = rt_read_file(path)
|
||||
if ptr_is_null(d) { spr_n = 0 return }
|
||||
let size = rt_file_len
|
||||
let pal = mem_alloc(128 * 4)
|
||||
mem_set(pal, 0, 128 * 4)
|
||||
let cur = 0 - 1
|
||||
let row = 0
|
||||
let i = 0
|
||||
while i < size {
|
||||
let start = i
|
||||
let len = 0
|
||||
let j = start
|
||||
while j < size {
|
||||
let ch = peek8(d, j)
|
||||
if ch == 10 { j = size } else { len = len + 1 j = j + 1 }
|
||||
}
|
||||
let c0 = peek8(d, start)
|
||||
if c0 == 112 { # 'p' — "pal <char> <rrggbb>"
|
||||
let ch = peek8(d, start + 4)
|
||||
let v = 0
|
||||
for k in 0 .. 6 {
|
||||
let hv = rt_hexval(peek8(d, start + 6 + k))
|
||||
if hv >= 0 { v = v * 16 + hv }
|
||||
}
|
||||
if ch < 128 { poke32(pal, ch, bor(shl(255, 24), v)) }
|
||||
}
|
||||
if c0 == 115 { # 's' — "spr" starts a new sprite
|
||||
cur = 0 - 1
|
||||
if spr_n < SPR_MAX {
|
||||
cur = spr_n
|
||||
spr_n = spr_n + 1
|
||||
mem_set(ptr_add(spr_px, cur * SPR_SZ * SPR_SZ * 4), 0, SPR_SZ * SPR_SZ * 4)
|
||||
}
|
||||
row = 0
|
||||
}
|
||||
if c0 != 112 {
|
||||
if c0 != 115 {
|
||||
if c0 != 35 { # '#' comment
|
||||
if cur >= 0 {
|
||||
if row < SPR_SZ {
|
||||
for x in 0 .. SPR_SZ {
|
||||
if x < len {
|
||||
poke32(spr_px, cur * SPR_SZ * SPR_SZ + row * SPR_SZ + x,
|
||||
peek32(pal, band(peek8(d, start + x), 127)))
|
||||
}
|
||||
}
|
||||
row = row + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
i = start + len + 1
|
||||
}
|
||||
mem_free(pal)
|
||||
mem_free(d)
|
||||
}
|
||||
276
runtime/native/inflate.ludic
Normal file
276
runtime/native/inflate.ludic
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
# ============================================================================
|
||||
# inflate.ludic — DEFLATE decompression (RFC 1951), written in Ludic.
|
||||
#
|
||||
# PNG stores its pixels zlib-compressed, so decoding one means implementing
|
||||
# inflate. The C runtime linked zlib for this. We don't: zlib is not present by
|
||||
# default on every target Ludic compiles for (Windows especially), and shipping
|
||||
# a dependency to read a sprite is a poor trade when the algorithm is this
|
||||
# small. So it lives here, in the language.
|
||||
#
|
||||
# The decoder is the canonical-Huffman formulation from Mark Adler's `puff`:
|
||||
# a symbol table plus per-length counts, walked one bit at a time. Slower than
|
||||
# a lookup-table decoder, and entirely fast enough to load sprites at startup.
|
||||
# ============================================================================
|
||||
|
||||
# ---- bit reader (DEFLATE packs bits least-significant-first) ---------------
|
||||
var z_src: ptr = ptr_null()
|
||||
var z_len: int = 0
|
||||
var z_pos: int = 0
|
||||
var z_bitbuf: int = 0
|
||||
var z_bitcnt: int = 0
|
||||
var z_err: int = 0
|
||||
|
||||
fn z_start(src: ptr, len: int) -> void {
|
||||
z_src = src
|
||||
z_len = len
|
||||
z_pos = 0
|
||||
z_bitbuf = 0
|
||||
z_bitcnt = 0
|
||||
z_err = 0
|
||||
}
|
||||
|
||||
fn z_bits(need: int) -> int {
|
||||
let val = z_bitbuf
|
||||
while z_bitcnt < need {
|
||||
if z_pos >= z_len {
|
||||
z_err = 1
|
||||
return 0
|
||||
}
|
||||
val = bor(val, shl(peek8(z_src, z_pos), z_bitcnt))
|
||||
z_pos = z_pos + 1
|
||||
z_bitcnt = z_bitcnt + 8
|
||||
}
|
||||
z_bitbuf = shr(val, need)
|
||||
z_bitcnt = z_bitcnt - need
|
||||
return band(val, shl(1, need) - 1)
|
||||
}
|
||||
|
||||
# ---- Huffman tables -------------------------------------------------------
|
||||
# A table is a single buffer: 16 length-counts followed by the symbols in
|
||||
# canonical order. One allocation, no structs.
|
||||
fn z_table_new(nsym: int) -> ptr {
|
||||
return mem_alloc((16 + nsym) * 4)
|
||||
}
|
||||
|
||||
# lengths[i] = code length of symbol i (0 = symbol unused)
|
||||
fn z_table_build(table: ptr, lengths: ptr, n: int) -> void {
|
||||
for i in 0 .. 16 {
|
||||
poke32(table, i, 0)
|
||||
}
|
||||
for s in 0 .. n {
|
||||
let l = peek32(lengths, s)
|
||||
poke32(table, l, peek32(table, l) + 1)
|
||||
}
|
||||
poke32(table, 0, 0) # length 0 means "not present"
|
||||
# offset of each length's first symbol
|
||||
let offs = mem_alloc(16 * 4)
|
||||
poke32(offs, 1, 0)
|
||||
for l in 1 .. 15 {
|
||||
poke32(offs, l + 1, peek32(offs, l) + peek32(table, l))
|
||||
}
|
||||
for s in 0 .. n {
|
||||
let l = peek32(lengths, s)
|
||||
if l != 0 {
|
||||
poke32(table, 16 + peek32(offs, l), s)
|
||||
poke32(offs, l, peek32(offs, l) + 1)
|
||||
}
|
||||
}
|
||||
mem_free(offs)
|
||||
}
|
||||
|
||||
fn z_decode(table: ptr) -> int {
|
||||
let code = 0
|
||||
let first = 0
|
||||
let index = 0
|
||||
for len in 1 .. 16 {
|
||||
code = bor(code, z_bits(1))
|
||||
let count = peek32(table, len)
|
||||
if code - first < count {
|
||||
return peek32(table, 16 + index + (code - first))
|
||||
}
|
||||
index = index + count
|
||||
first = shl(first + count, 1)
|
||||
code = shl(code, 1)
|
||||
}
|
||||
z_err = 1
|
||||
return -1
|
||||
}
|
||||
|
||||
# ---- length / distance code tables (RFC 1951 section 3.2.5) ---------------
|
||||
fn z_len_base(sym: int) -> int {
|
||||
if sym < 8 { return 3 + sym }
|
||||
if sym == 28 { return 258 }
|
||||
let extra = (sym - 4) / 4
|
||||
let group = shl(1, extra)
|
||||
return 3 + shl(group - 1, 2) + 4 + (sym - 4 - extra * 4) * group
|
||||
}
|
||||
|
||||
fn z_len_extra(sym: int) -> int {
|
||||
if sym < 8 { return 0 }
|
||||
if sym == 28 { return 0 }
|
||||
return (sym - 4) / 4
|
||||
}
|
||||
|
||||
fn z_dist_base(sym: int) -> int {
|
||||
if sym < 4 { return 1 + sym }
|
||||
let extra = (sym - 2) / 2
|
||||
let group = shl(1, extra)
|
||||
return 1 + shl(group, 1) + (sym - 2 - extra * 2) * group
|
||||
}
|
||||
|
||||
fn z_dist_extra(sym: int) -> int {
|
||||
if sym < 4 { return 0 }
|
||||
return (sym - 2) / 2
|
||||
}
|
||||
|
||||
# ---- block decoders -------------------------------------------------------
|
||||
# `out` is the destination window; returns the new write position, or -1.
|
||||
fn z_stored(out: ptr, at: int, cap: int) -> int {
|
||||
z_bitbuf = 0
|
||||
z_bitcnt = 0 # stored blocks are byte-aligned
|
||||
if z_pos + 4 > z_len { return -1 }
|
||||
let n = peek8(z_src, z_pos) + shl(peek8(z_src, z_pos + 1), 8)
|
||||
z_pos = z_pos + 4 # LEN then its one's complement
|
||||
let w = at
|
||||
for i in 0 .. n {
|
||||
if z_pos >= z_len { return -1 }
|
||||
if w >= cap { return -1 }
|
||||
poke8(out, w, peek8(z_src, z_pos))
|
||||
w = w + 1
|
||||
z_pos = z_pos + 1
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
fn z_codes(out: ptr, at: int, cap: int, lit: ptr, dist: ptr) -> int {
|
||||
let w = at
|
||||
let sym = z_decode(lit)
|
||||
while sym != 256 {
|
||||
if z_err != 0 { return -1 }
|
||||
if sym < 0 { return -1 }
|
||||
if sym < 256 {
|
||||
if w >= cap { return -1 }
|
||||
poke8(out, w, sym)
|
||||
w = w + 1
|
||||
}
|
||||
if sym > 256 {
|
||||
let s = sym - 257
|
||||
if s >= 29 { return -1 }
|
||||
let length = z_len_base(s) + z_bits(z_len_extra(s))
|
||||
let d = z_decode(dist)
|
||||
if d < 0 { return -1 }
|
||||
let distance = z_dist_base(d) + z_bits(z_dist_extra(d))
|
||||
if distance > w { return -1 }
|
||||
for k in 0 .. length {
|
||||
if w >= cap { return -1 }
|
||||
poke8(out, w, peek8(out, w - distance))
|
||||
w = w + 1
|
||||
}
|
||||
}
|
||||
sym = z_decode(lit)
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
fn z_fixed_tables(lit: ptr, dist: ptr) -> void {
|
||||
let lengths = mem_alloc(288 * 4)
|
||||
for i in 0 .. 144 { poke32(lengths, i, 8) }
|
||||
for i in 144 .. 256 { poke32(lengths, i, 9) }
|
||||
for i in 256 .. 280 { poke32(lengths, i, 7) }
|
||||
for i in 280 .. 288 { poke32(lengths, i, 8) }
|
||||
z_table_build(lit, lengths, 288)
|
||||
for i in 0 .. 30 { poke32(lengths, i, 5) }
|
||||
z_table_build(dist, lengths, 30)
|
||||
mem_free(lengths)
|
||||
}
|
||||
|
||||
fn z_dynamic_tables(lit: ptr, dist: ptr) -> int {
|
||||
let nlen = z_bits(5) + 257
|
||||
let ndist = z_bits(5) + 1
|
||||
let ncode = z_bits(4) + 4
|
||||
if nlen > 286 { return 0 }
|
||||
if ndist > 30 { return 0 }
|
||||
|
||||
let lengths = mem_alloc(320 * 4)
|
||||
for i in 0 .. 19 { poke32(lengths, i, 0) }
|
||||
# the code-length alphabet is transmitted in this fixed permutation
|
||||
# 16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15 — biased by '0' so it is one literal
|
||||
let order = "@AB08796:5;4<3=2>1?"
|
||||
for i in 0 .. ncode {
|
||||
poke32(lengths, peek8(order, i) - 48, z_bits(3))
|
||||
}
|
||||
let clen = z_table_new(19)
|
||||
z_table_build(clen, lengths, 19)
|
||||
|
||||
let n = 0
|
||||
while n < nlen + ndist {
|
||||
let sym = z_decode(clen)
|
||||
if sym < 0 { return 0 }
|
||||
if sym < 16 {
|
||||
poke32(lengths, n, sym)
|
||||
n = n + 1
|
||||
}
|
||||
if sym >= 16 {
|
||||
let prev = 0
|
||||
let rep = 0
|
||||
if sym == 16 {
|
||||
if n == 0 { return 0 }
|
||||
prev = peek32(lengths, n - 1)
|
||||
rep = 3 + z_bits(2)
|
||||
}
|
||||
if sym == 17 { rep = 3 + z_bits(3) }
|
||||
if sym == 18 { rep = 11 + z_bits(7) }
|
||||
for k in 0 .. rep {
|
||||
if n < 320 {
|
||||
poke32(lengths, n, prev)
|
||||
n = n + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
z_table_build(lit, lengths, nlen)
|
||||
# the distance lengths follow the literal ones in the same buffer
|
||||
let dl = mem_alloc(32 * 4)
|
||||
for i in 0 .. ndist { poke32(dl, i, peek32(lengths, nlen + i)) }
|
||||
z_table_build(dist, dl, ndist)
|
||||
mem_free(dl)
|
||||
mem_free(lengths)
|
||||
mem_free(clen)
|
||||
return 1
|
||||
}
|
||||
|
||||
# Inflate a raw DEFLATE stream. Returns bytes written, or -1.
|
||||
fn z_inflate(src: ptr, len: int, out: ptr, cap: int) -> int {
|
||||
z_start(src, len)
|
||||
let lit = z_table_new(288)
|
||||
let dist = z_table_new(30)
|
||||
let w = 0
|
||||
let final = 0
|
||||
while final == 0 {
|
||||
final = z_bits(1)
|
||||
let btype = z_bits(2)
|
||||
if z_err != 0 { return -1 }
|
||||
if btype == 0 { w = z_stored(out, w, cap) }
|
||||
if btype == 1 {
|
||||
z_fixed_tables(lit, dist)
|
||||
w = z_codes(out, w, cap, lit, dist)
|
||||
}
|
||||
if btype == 2 {
|
||||
if z_dynamic_tables(lit, dist) == 0 { return -1 }
|
||||
w = z_codes(out, w, cap, lit, dist)
|
||||
}
|
||||
if btype == 3 { return -1 }
|
||||
if w < 0 { return -1 }
|
||||
}
|
||||
mem_free(lit)
|
||||
mem_free(dist)
|
||||
return w
|
||||
}
|
||||
|
||||
# zlib wrapper (RFC 1950): two header bytes, then DEFLATE, then Adler-32.
|
||||
fn z_uncompress(src: ptr, len: int, out: ptr, cap: int) -> int {
|
||||
if len < 2 { return -1 }
|
||||
let cmf = peek8(src, 0)
|
||||
if band(cmf, 15) != 8 { return -1 }
|
||||
return z_inflate(ptr_add(src, 2), len - 2, out, cap)
|
||||
}
|
||||
428
runtime/native/truetype.ludic
Normal file
428
runtime/native/truetype.ludic
Normal file
|
|
@ -0,0 +1,428 @@
|
|||
# ============================================================================
|
||||
# truetype.ludic — a TrueType font loader and glyph rasterizer, in Ludic.
|
||||
#
|
||||
# Parses the sfnt container, maps codepoints through cmap (formats 0/4/6/12),
|
||||
# decodes simple and composite `glyf` outlines, flattens the quadratic Béziers,
|
||||
# and fills them with the nonzero winding rule at 4x4 supersampling to get an
|
||||
# 8-bit coverage mask. Then a glyph cache and a UTF-8 text layer on top.
|
||||
#
|
||||
# The C original used floats. Ludic has Q16.16 `fixed`, which is enough: at 16
|
||||
# fractional bits a glyph coordinate is exact to 1/65536 of a pixel, far below
|
||||
# the 1/4-pixel supersample grid the rasterizer actually quantises to.
|
||||
# ============================================================================
|
||||
|
||||
const TT_MAX: int = 8 # fonts loaded at once
|
||||
const TT_SS: int = 4 # supersample factor per axis
|
||||
const TT_PTS: int = 4096 # points in one outline
|
||||
const TT_EDGES: int = 16384 # line segments in one glyph
|
||||
const TT_GC: int = 512 # glyph cache entries
|
||||
|
||||
# ---- per-font tables (parallel arrays; index = font id) -------------------
|
||||
var tt_data: ptr = ptr_null()
|
||||
var tt_size: ptr = ptr_null()
|
||||
var tt_upem: ptr = ptr_null()
|
||||
var tt_nglyf: ptr = ptr_null()
|
||||
var tt_locfm: ptr = ptr_null()
|
||||
var tt_nhm: ptr = ptr_null()
|
||||
var tt_asc: ptr = ptr_null()
|
||||
var tt_desc: ptr = ptr_null()
|
||||
var tt_gap: ptr = ptr_null()
|
||||
var tt_glyf: ptr = ptr_null()
|
||||
var tt_loca: ptr = ptr_null()
|
||||
var tt_hmtx: ptr = ptr_null()
|
||||
var tt_cmap: ptr = ptr_null()
|
||||
var tt_cfmt: ptr = ptr_null()
|
||||
var tt_n: int = 0
|
||||
|
||||
# ---- outline + edge scratch (reused for every glyph) ----------------------
|
||||
var ol_x: ptr = ptr_null()
|
||||
var ol_y: ptr = ptr_null()
|
||||
var ol_on: ptr = ptr_null()
|
||||
var ol_ends: ptr = ptr_null()
|
||||
var ol_n: int = 0
|
||||
var ol_ne: int = 0
|
||||
|
||||
var ed_x0: ptr = ptr_null()
|
||||
var ed_y0: ptr = ptr_null()
|
||||
var ed_x1: ptr = ptr_null()
|
||||
var ed_y1: ptr = ptr_null()
|
||||
var ed_n: int = 0
|
||||
|
||||
var sc_x: ptr = ptr_null() # scanline crossings
|
||||
var sc_d: ptr = ptr_null()
|
||||
|
||||
# ---- glyph cache ----------------------------------------------------------
|
||||
var gc_used: ptr = ptr_null()
|
||||
var gc_font: ptr = ptr_null()
|
||||
var gc_cp: ptr = ptr_null()
|
||||
var gc_px: ptr = ptr_null()
|
||||
var gc_w: ptr = ptr_null()
|
||||
var gc_h: ptr = ptr_null()
|
||||
var gc_ox: ptr = ptr_null()
|
||||
var gc_oy: ptr = ptr_null()
|
||||
var gc_adv: ptr = ptr_null()
|
||||
var gc_bmp: ptr = ptr_null()
|
||||
|
||||
fn rt_tt_init() -> void {
|
||||
tt_data = mem_alloc(TT_MAX * 8)
|
||||
tt_size = mem_alloc(TT_MAX * 4)
|
||||
tt_upem = mem_alloc(TT_MAX * 4)
|
||||
tt_nglyf = mem_alloc(TT_MAX * 4)
|
||||
tt_locfm = mem_alloc(TT_MAX * 4)
|
||||
tt_nhm = mem_alloc(TT_MAX * 4)
|
||||
tt_asc = mem_alloc(TT_MAX * 4)
|
||||
tt_desc = mem_alloc(TT_MAX * 4)
|
||||
tt_gap = mem_alloc(TT_MAX * 4)
|
||||
tt_glyf = mem_alloc(TT_MAX * 4)
|
||||
tt_loca = mem_alloc(TT_MAX * 4)
|
||||
tt_hmtx = mem_alloc(TT_MAX * 4)
|
||||
tt_cmap = mem_alloc(TT_MAX * 4)
|
||||
tt_cfmt = mem_alloc(TT_MAX * 4)
|
||||
|
||||
ol_x = mem_alloc(TT_PTS * 4)
|
||||
ol_y = mem_alloc(TT_PTS * 4)
|
||||
ol_on = mem_alloc(TT_PTS * 4)
|
||||
ol_ends = mem_alloc(256 * 4)
|
||||
|
||||
ed_x0 = mem_alloc(TT_EDGES * 4)
|
||||
ed_y0 = mem_alloc(TT_EDGES * 4)
|
||||
ed_x1 = mem_alloc(TT_EDGES * 4)
|
||||
ed_y1 = mem_alloc(TT_EDGES * 4)
|
||||
sc_x = mem_alloc(TT_EDGES * 4)
|
||||
sc_d = mem_alloc(TT_EDGES * 4)
|
||||
|
||||
gc_used = mem_alloc(TT_GC * 4)
|
||||
gc_font = mem_alloc(TT_GC * 4)
|
||||
gc_cp = mem_alloc(TT_GC * 4)
|
||||
gc_px = mem_alloc(TT_GC * 4)
|
||||
gc_w = mem_alloc(TT_GC * 4)
|
||||
gc_h = mem_alloc(TT_GC * 4)
|
||||
gc_ox = mem_alloc(TT_GC * 4)
|
||||
gc_oy = mem_alloc(TT_GC * 4)
|
||||
gc_adv = mem_alloc(TT_GC * 4)
|
||||
gc_bmp = mem_alloc(TT_GC * 8)
|
||||
mem_set(gc_used, 0, TT_GC * 4)
|
||||
}
|
||||
|
||||
# ---- big-endian readers ---------------------------------------------------
|
||||
fn tt_u16(d: ptr, at: int) -> int {
|
||||
return bor(shl(peek8(d, at), 8), peek8(d, at + 1))
|
||||
}
|
||||
fn tt_i16(d: ptr, at: int) -> int {
|
||||
let v = tt_u16(d, at)
|
||||
if v >= 32768 { return v - 65536 }
|
||||
return v
|
||||
}
|
||||
fn tt_u32(d: ptr, at: int) -> int {
|
||||
return bor(bor(shl(peek8(d, at), 24), shl(peek8(d, at + 1), 16)),
|
||||
bor(shl(peek8(d, at + 2), 8), peek8(d, at + 3)))
|
||||
}
|
||||
fn tt_i8(d: ptr, at: int) -> int {
|
||||
let v = peek8(d, at)
|
||||
if v >= 128 { return v - 256 }
|
||||
return v
|
||||
}
|
||||
|
||||
fn tt_find_table(d: ptr, base: int, a: int, b: int, c: int, e: int) -> int {
|
||||
let n = tt_u16(d, base + 4)
|
||||
for i in 0 .. n {
|
||||
let rec = base + 12 + i * 16
|
||||
if peek8(d, rec) == a {
|
||||
if peek8(d, rec + 1) == b {
|
||||
if peek8(d, rec + 2) == c {
|
||||
if peek8(d, rec + 3) == e { return tt_u32(d, rec + 8) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0 - 1
|
||||
}
|
||||
|
||||
# pick the most capable Unicode subtable, as the C loader did
|
||||
fn tt_pick_cmap(id: int, d: ptr, co: int) -> void {
|
||||
poke32(tt_cmap, id, 0 - 1)
|
||||
poke32(tt_cfmt, id, 0)
|
||||
if co < 0 { return }
|
||||
let n = tt_u16(d, co + 2)
|
||||
let best = 0 - 1
|
||||
let bestscore = 0 - 1
|
||||
for i in 0 .. n {
|
||||
let rec = co + 4 + i * 8
|
||||
let plat = tt_u16(d, rec)
|
||||
let enc = tt_u16(d, rec + 2)
|
||||
let off = tt_u32(d, rec + 4)
|
||||
let score = 0 - 1
|
||||
if plat == 0 { score = 2 }
|
||||
if plat == 0 { if enc == 4 { score = 4 } }
|
||||
if plat == 0 { if enc == 6 { score = 4 } }
|
||||
if plat == 3 { if enc == 1 { score = 3 } }
|
||||
if plat == 3 { if enc == 10 { score = 5 } }
|
||||
if score > bestscore {
|
||||
bestscore = score
|
||||
best = co + off
|
||||
}
|
||||
}
|
||||
if best < 0 { return }
|
||||
poke32(tt_cmap, id, best)
|
||||
poke32(tt_cfmt, id, tt_u16(d, best))
|
||||
}
|
||||
|
||||
fn rt_font_load(path: str) -> int {
|
||||
if tt_n >= TT_MAX { return 0 - 1 }
|
||||
let d = rt_read_file(path)
|
||||
if ptr_is_null(d) { return 0 - 1 }
|
||||
let size = rt_file_len
|
||||
let base = 0
|
||||
# a .ttc collection points at its first font
|
||||
if size >= 16 {
|
||||
if peek8(d, 0) == 116 { if peek8(d, 1) == 116 { if peek8(d, 2) == 99 { if peek8(d, 3) == 102 {
|
||||
base = tt_u32(d, 12)
|
||||
} } } }
|
||||
}
|
||||
let head = tt_find_table(d, base, 104, 101, 97, 100)
|
||||
let maxp = tt_find_table(d, base, 109, 97, 120, 112)
|
||||
let hhea = tt_find_table(d, base, 104, 104, 101, 97)
|
||||
let loca = tt_find_table(d, base, 108, 111, 99, 97)
|
||||
let glyf = tt_find_table(d, base, 103, 108, 121, 102)
|
||||
let hmtx = tt_find_table(d, base, 104, 109, 116, 120)
|
||||
if head < 0 { return 0 - 1 }
|
||||
if maxp < 0 { return 0 - 1 }
|
||||
if hhea < 0 { return 0 - 1 }
|
||||
if loca < 0 { return 0 - 1 }
|
||||
if glyf < 0 { return 0 - 1 }
|
||||
if hmtx < 0 { return 0 - 1 }
|
||||
|
||||
let id = tt_n
|
||||
tt_n = tt_n + 1
|
||||
pokep(tt_data, id, d)
|
||||
poke32(tt_size, id, size)
|
||||
let upem = tt_u16(d, head + 18)
|
||||
if upem <= 0 { upem = 1000 }
|
||||
poke32(tt_upem, id, upem)
|
||||
poke32(tt_locfm, id, tt_i16(d, head + 50))
|
||||
poke32(tt_nglyf, id, tt_u16(d, maxp + 4))
|
||||
poke32(tt_asc, id, tt_i16(d, hhea + 4))
|
||||
poke32(tt_desc, id, tt_i16(d, hhea + 6))
|
||||
poke32(tt_gap, id, tt_i16(d, hhea + 8))
|
||||
poke32(tt_nhm, id, tt_u16(d, hhea + 34))
|
||||
poke32(tt_glyf, id, glyf)
|
||||
poke32(tt_loca, id, loca)
|
||||
poke32(tt_hmtx, id, hmtx)
|
||||
tt_pick_cmap(id, d, tt_find_table(d, base, 99, 109, 97, 112))
|
||||
return id
|
||||
}
|
||||
|
||||
# ---- codepoint -> glyph id ------------------------------------------------
|
||||
fn tt_glyph_index(id: int, cp: int) -> int {
|
||||
let d = peekp(tt_data, id)
|
||||
let s = peek32(tt_cmap, id)
|
||||
if s < 0 { return 0 }
|
||||
let fmt = peek32(tt_cfmt, id)
|
||||
|
||||
if fmt == 4 {
|
||||
if cp > 65535 { return 0 }
|
||||
let segX2 = tt_u16(d, s + 6)
|
||||
let segc = segX2 / 2
|
||||
let endp = s + 14
|
||||
let startp = endp + segX2 + 2
|
||||
let deltap = startp + segX2
|
||||
let rangep = deltap + segX2
|
||||
for i in 0 .. segc {
|
||||
if cp <= tt_u16(d, endp + i * 2) {
|
||||
let st = tt_u16(d, startp + i * 2)
|
||||
if cp < st { return 0 }
|
||||
let ro = tt_u16(d, rangep + i * 2)
|
||||
if ro == 0 { return band(cp + tt_i16(d, deltap + i * 2), 65535) }
|
||||
let g = tt_u16(d, rangep + i * 2 + ro + (cp - st) * 2)
|
||||
if g == 0 { return 0 }
|
||||
return band(g + tt_i16(d, deltap + i * 2), 65535)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
if fmt == 12 {
|
||||
let ng = tt_u32(d, s + 12)
|
||||
for i in 0 .. ng {
|
||||
let g = s + 16 + i * 12
|
||||
if cp >= tt_u32(d, g) {
|
||||
if cp <= tt_u32(d, g + 4) { return tt_u32(d, g + 8) + (cp - tt_u32(d, g)) }
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
if fmt == 6 {
|
||||
let first = tt_u16(d, s + 6)
|
||||
let cnt = tt_u16(d, s + 8)
|
||||
if cp >= first { if cp < first + cnt { return tt_u16(d, s + 10 + (cp - first) * 2) } }
|
||||
return 0
|
||||
}
|
||||
if fmt == 0 {
|
||||
if cp < 256 { return peek8(d, s + 6 + cp) }
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
fn tt_advance(id: int, gid: int) -> int {
|
||||
let d = peekp(tt_data, id)
|
||||
let hmtx = peek32(tt_hmtx, id)
|
||||
let n = peek32(tt_nhm, id)
|
||||
if gid < n { return tt_u16(d, hmtx + gid * 4) }
|
||||
return tt_u16(d, hmtx + (n - 1) * 4)
|
||||
}
|
||||
|
||||
# ---- outline extraction ---------------------------------------------------
|
||||
fn ol_pt(x: fixed, y: fixed, on: int) -> void {
|
||||
if ol_n >= TT_PTS { return }
|
||||
pokef(ol_x, ol_n, x)
|
||||
pokef(ol_y, ol_n, y)
|
||||
poke32(ol_on, ol_n, on)
|
||||
ol_n = ol_n + 1
|
||||
}
|
||||
|
||||
fn tt_glyph_start(id: int, gid: int) -> int {
|
||||
let d = peekp(tt_data, id)
|
||||
let loca = peek32(tt_loca, id)
|
||||
if peek32(tt_locfm, id) == 0 { return tt_u16(d, loca + gid * 2) * 2 }
|
||||
return tt_u32(d, loca + gid * 4)
|
||||
}
|
||||
fn tt_glyph_end(id: int, gid: int) -> int {
|
||||
let d = peekp(tt_data, id)
|
||||
let loca = peek32(tt_loca, id)
|
||||
if peek32(tt_locfm, id) == 0 { return tt_u16(d, loca + (gid + 1) * 2) * 2 }
|
||||
return tt_u32(d, loca + (gid + 1) * 4)
|
||||
}
|
||||
|
||||
# Append glyph `gid`, transformed by [a b c e] + (dx,dy), to the outline.
|
||||
fn tt_load_outline(id: int, gid: int, a: fixed, b: fixed, c: fixed, e: fixed, dx: fixed, dy: fixed, depth: int) -> void {
|
||||
if gid < 0 { return }
|
||||
if gid >= peek32(tt_nglyf, id) { return }
|
||||
if depth > 5 { return }
|
||||
let goff = tt_glyph_start(id, gid)
|
||||
let gend = tt_glyph_end(id, gid)
|
||||
if goff >= gend { return }
|
||||
let d = peekp(tt_data, id)
|
||||
let g = peek32(tt_glyf, id) + goff
|
||||
let nc = tt_i16(d, g)
|
||||
|
||||
if nc >= 0 {
|
||||
let endpts = g + 10
|
||||
let npts = 0
|
||||
if nc > 0 { npts = tt_u16(d, endpts + (nc - 1) * 2) + 1 }
|
||||
let p = endpts + nc * 2
|
||||
p = p + 2 + tt_u16(d, p) # skip instructions
|
||||
|
||||
let flags = mem_alloc(npts + 8)
|
||||
let i = 0
|
||||
while i < npts {
|
||||
let fl = peek8(d, p)
|
||||
p = p + 1
|
||||
poke8(flags, i, fl)
|
||||
i = i + 1
|
||||
if band(fl, 8) != 0 { # REPEAT
|
||||
let r = peek8(d, p)
|
||||
p = p + 1
|
||||
while r > 0 {
|
||||
if i < npts { poke8(flags, i, fl) i = i + 1 }
|
||||
r = r - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let xs = mem_alloc((npts + 1) * 4)
|
||||
let ys = mem_alloc((npts + 1) * 4)
|
||||
let xv = 0
|
||||
for k in 0 .. npts {
|
||||
let fl = peek8(flags, k)
|
||||
if band(fl, 2) != 0 {
|
||||
let dxv = peek8(d, p)
|
||||
p = p + 1
|
||||
if band(fl, 16) != 0 { xv = xv + dxv } else { xv = xv - dxv }
|
||||
} else {
|
||||
if band(fl, 16) == 0 { xv = xv + tt_i16(d, p) p = p + 2 }
|
||||
}
|
||||
poke32(xs, k, xv)
|
||||
}
|
||||
let yv = 0
|
||||
for k in 0 .. npts {
|
||||
let fl = peek8(flags, k)
|
||||
if band(fl, 4) != 0 {
|
||||
let dyv = peek8(d, p)
|
||||
p = p + 1
|
||||
if band(fl, 32) != 0 { yv = yv + dyv } else { yv = yv - dyv }
|
||||
} else {
|
||||
if band(fl, 32) == 0 { yv = yv + tt_i16(d, p) p = p + 2 }
|
||||
}
|
||||
poke32(ys, k, yv)
|
||||
}
|
||||
|
||||
let start = 0
|
||||
for ci in 0 .. nc {
|
||||
let last = tt_u16(d, endpts + ci * 2)
|
||||
for k in start .. last + 1 {
|
||||
let X = fx(peek32(xs, k))
|
||||
let Y = fx(peek32(ys, k))
|
||||
ol_pt(a * X + c * Y + dx, b * X + e * Y + dy, band(peek8(flags, k), 1))
|
||||
}
|
||||
if ol_ne < 256 {
|
||||
poke32(ol_ends, ol_ne, ol_n)
|
||||
ol_ne = ol_ne + 1
|
||||
}
|
||||
start = last + 1
|
||||
}
|
||||
mem_free(flags)
|
||||
mem_free(xs)
|
||||
mem_free(ys)
|
||||
} else {
|
||||
# composite glyph: components, each with its own placement and transform
|
||||
let p = g + 10
|
||||
let more = 1
|
||||
while more == 1 {
|
||||
let flags = tt_u16(d, p)
|
||||
let cgid = tt_u16(d, p + 2)
|
||||
p = p + 4
|
||||
let arg1 = 0
|
||||
let arg2 = 0
|
||||
if band(flags, 1) != 0 {
|
||||
arg1 = tt_i16(d, p)
|
||||
arg2 = tt_i16(d, p + 2)
|
||||
p = p + 4
|
||||
} else {
|
||||
arg1 = tt_i8(d, p)
|
||||
arg2 = tt_i8(d, p + 1)
|
||||
p = p + 2
|
||||
}
|
||||
let ca = 1.0
|
||||
let cb = 0.0
|
||||
let cc = 0.0
|
||||
let ce = 1.0
|
||||
if band(flags, 8) != 0 {
|
||||
ca = as_fixed(shl(tt_i16(d, p), 2)) # F2Dot14 -> Q16.16
|
||||
ce = ca
|
||||
p = p + 2
|
||||
}
|
||||
if band(flags, 64) != 0 {
|
||||
ca = as_fixed(shl(tt_i16(d, p), 2))
|
||||
ce = as_fixed(shl(tt_i16(d, p + 2), 2))
|
||||
p = p + 4
|
||||
}
|
||||
if band(flags, 128) != 0 {
|
||||
ca = as_fixed(shl(tt_i16(d, p), 2))
|
||||
cb = as_fixed(shl(tt_i16(d, p + 2), 2))
|
||||
cc = as_fixed(shl(tt_i16(d, p + 4), 2))
|
||||
ce = as_fixed(shl(tt_i16(d, p + 6), 2))
|
||||
p = p + 8
|
||||
}
|
||||
let odx = dx
|
||||
let ody = dy
|
||||
if band(flags, 2) != 0 {
|
||||
odx = a * fx(arg1) + c * fx(arg2) + dx
|
||||
ody = b * fx(arg1) + e * fx(arg2) + dy
|
||||
}
|
||||
tt_load_outline(id, cgid, a * ca + c * cb, b * ca + e * cb, a * cc + c * ce, b * cc + e * ce, odx, ody, depth + 1)
|
||||
more = 0
|
||||
if band(flags, 32) != 0 { more = 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
import "truetype_raster.ludic"
|
||||
380
runtime/native/truetype_raster.ludic
Normal file
380
runtime/native/truetype_raster.ludic
Normal file
|
|
@ -0,0 +1,380 @@
|
|||
# truetype_raster.ludic — glyph rasterization, the glyph cache, UTF-8 decoding
|
||||
# and text drawing. Split out of truetype.ludic (the sfnt/cmap/outline half).
|
||||
|
||||
# ---- rasterization --------------------------------------------------------
|
||||
fn ed_add(x0: fixed, y0: fixed, x1: fixed, y1: fixed) -> void {
|
||||
if ed_n >= TT_EDGES { return }
|
||||
pokef(ed_x0, ed_n, x0)
|
||||
pokef(ed_y0, ed_n, y0)
|
||||
pokef(ed_x1, ed_n, x1)
|
||||
pokef(ed_y1, ed_n, y1)
|
||||
ed_n = ed_n + 1
|
||||
}
|
||||
|
||||
fn tt_isqrt(v: int) -> int {
|
||||
if v <= 0 { return 0 }
|
||||
let r = 0
|
||||
let b = 32768
|
||||
while b > 0 {
|
||||
let t = r + b
|
||||
if t * t <= v { r = t }
|
||||
b = b / 2
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
# flatten one quadratic Bézier into line segments, subdivided by chord length
|
||||
fn ed_quad(x0: fixed, y0: fixed, cx: fixed, cy: fixed, x1: fixed, y1: fixed) -> void {
|
||||
let dx = flr(x1) - flr(x0)
|
||||
let dy = flr(y1) - flr(y0)
|
||||
let n = tt_isqrt(dx * dx + dy * dy) / 3
|
||||
if n < 2 { n = 2 }
|
||||
if n > 24 { n = 24 }
|
||||
let px = x0
|
||||
let py = y0
|
||||
for i in 1 .. n + 1 {
|
||||
let t = fx(i) / n
|
||||
let u = 1.0 - t
|
||||
let qx = u * u * x0 + 2.0 * u * t * cx + t * t * x1
|
||||
let qy = u * u * y0 + 2.0 * u * t * cy + t * t * y1
|
||||
ed_add(px, py, qx, qy)
|
||||
px = qx
|
||||
py = qy
|
||||
}
|
||||
}
|
||||
|
||||
# Rasterize a glyph at `px` pixels. Coverage lands in a fresh buffer; the
|
||||
# geometry is left in gr_* for the caller.
|
||||
var gr_w: int = 0
|
||||
var gr_h: int = 0
|
||||
var gr_ox: int = 0
|
||||
var gr_oy: int = 0
|
||||
var gr_adv: int = 0
|
||||
|
||||
fn tt_raster(id: int, gid: int, px: int) -> ptr {
|
||||
let upem = peek32(tt_upem, id)
|
||||
let scale = fx(px) / upem
|
||||
gr_adv = flr(fx(tt_advance(id, gid)) * scale + 0.5)
|
||||
gr_w = 0
|
||||
gr_h = 0
|
||||
ol_n = 0
|
||||
ol_ne = 0
|
||||
ed_n = 0
|
||||
tt_load_outline(id, gid, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0)
|
||||
if ol_n == 0 { return ptr_null() }
|
||||
|
||||
let minx = 999999.0
|
||||
let miny = 999999.0
|
||||
let maxx = 0.0 - 999999.0
|
||||
let maxy = 0.0 - 999999.0
|
||||
for i in 0 .. ol_n {
|
||||
let X = peekf(ol_x, i) * scale
|
||||
let Y = peekf(ol_y, i) * scale
|
||||
if X < minx { minx = X }
|
||||
if X > maxx { maxx = X }
|
||||
if Y < miny { miny = Y }
|
||||
if Y > maxy { maxy = Y }
|
||||
}
|
||||
let x0 = flr(minx)
|
||||
let y0 = flr(miny)
|
||||
let x1 = flr(maxx) + 1
|
||||
let y1 = flr(maxy) + 1
|
||||
if maxx == fx(flr(maxx)) { x1 = flr(maxx) }
|
||||
if maxy == fx(flr(maxy)) { y1 = flr(maxy) }
|
||||
let W = x1 - x0
|
||||
let H = y1 - y0
|
||||
if W <= 0 { return ptr_null() }
|
||||
if H <= 0 { return ptr_null() }
|
||||
if W > 1024 { return ptr_null() }
|
||||
if H > 1024 { return ptr_null() }
|
||||
gr_w = W
|
||||
gr_h = H
|
||||
gr_ox = x0
|
||||
gr_oy = y1
|
||||
|
||||
# build edges in supersample space (y grows downward)
|
||||
let start = 0
|
||||
for ci in 0 .. ol_ne {
|
||||
let end = peek32(ol_ends, ci)
|
||||
let cnt = end - start
|
||||
if cnt >= 2 {
|
||||
let first_on = 0 - 1
|
||||
for i in 0 .. cnt {
|
||||
if first_on < 0 { if peek32(ol_on, start + i) == 1 { first_on = i } }
|
||||
}
|
||||
let sx = 0.0
|
||||
let sy = 0.0
|
||||
if first_on < 0 {
|
||||
# all off-curve: start at the midpoint of the first and last point
|
||||
let mx = (peekf(ol_x, start) + peekf(ol_x, start + cnt - 1)) / 2
|
||||
let my = (peekf(ol_y, start) + peekf(ol_y, start + cnt - 1)) / 2
|
||||
sx = (mx * scale - fx(x0)) * TT_SS
|
||||
sy = (fx(y1) - my * scale) * TT_SS
|
||||
first_on = 0
|
||||
} else {
|
||||
sx = (peekf(ol_x, start + first_on) * scale - fx(x0)) * TT_SS
|
||||
sy = (fx(y1) - peekf(ol_y, start + first_on) * scale) * TT_SS
|
||||
}
|
||||
let curx = sx
|
||||
let cury = sy
|
||||
let step = 0
|
||||
while step < cnt {
|
||||
let i = (first_on + 1 + step) % cnt
|
||||
let ix = (peekf(ol_x, start + i) * scale - fx(x0)) * TT_SS
|
||||
let iy = (fx(y1) - peekf(ol_y, start + i) * scale) * TT_SS
|
||||
if peek32(ol_on, start + i) == 1 {
|
||||
ed_add(curx, cury, ix, iy)
|
||||
curx = ix
|
||||
cury = iy
|
||||
} else {
|
||||
let j = (i + 1) % cnt
|
||||
let jx = (peekf(ol_x, start + j) * scale - fx(x0)) * TT_SS
|
||||
let jy = (fx(y1) - peekf(ol_y, start + j) * scale) * TT_SS
|
||||
let ex = jx
|
||||
let ey = jy
|
||||
if peek32(ol_on, start + j) == 1 {
|
||||
step = step + 1
|
||||
} else {
|
||||
ex = (ix + jx) / 2 # implied on-curve midpoint
|
||||
ey = (iy + jy) / 2
|
||||
}
|
||||
ed_quad(curx, cury, ix, iy, ex, ey)
|
||||
curx = ex
|
||||
cury = ey
|
||||
}
|
||||
step = step + 1
|
||||
}
|
||||
ed_add(curx, cury, sx, sy)
|
||||
}
|
||||
start = end
|
||||
}
|
||||
|
||||
let SW = W * TT_SS
|
||||
let SH = H * TT_SS
|
||||
let cover = mem_alloc(W * H)
|
||||
mem_set(cover, 0, W * H)
|
||||
let unit = 255 / (TT_SS * TT_SS)
|
||||
|
||||
for sy in 0 .. SH {
|
||||
let yc = fx(sy) + 0.5
|
||||
let m = 0
|
||||
for i in 0 .. ed_n {
|
||||
let ya = peekf(ed_y0, i)
|
||||
let yb = peekf(ed_y1, i)
|
||||
let hit = 0
|
||||
if ya <= yc { if yb > yc { hit = 1 } }
|
||||
if yb <= yc { if ya > yc { hit = 1 } }
|
||||
if hit == 1 {
|
||||
let t = (yc - ya) / (yb - ya)
|
||||
pokef(sc_x, m, peekf(ed_x0, i) + t * (peekf(ed_x1, i) - peekf(ed_x0, i)))
|
||||
if yb > ya { poke32(sc_d, m, 1) } else { poke32(sc_d, m, 0 - 1) }
|
||||
m = m + 1
|
||||
}
|
||||
}
|
||||
# insertion sort the crossings by x
|
||||
for i in 1 .. m {
|
||||
let kx = peekf(sc_x, i)
|
||||
let kd = peek32(sc_d, i)
|
||||
let j = i - 1
|
||||
let placed = 0
|
||||
while j >= 0 {
|
||||
if peekf(sc_x, j) > kx {
|
||||
pokef(sc_x, j + 1, peekf(sc_x, j))
|
||||
poke32(sc_d, j + 1, peek32(sc_d, j))
|
||||
j = j - 1
|
||||
} else {
|
||||
pokef(sc_x, j + 1, kx)
|
||||
poke32(sc_d, j + 1, kd)
|
||||
placed = 1
|
||||
j = 0 - 1
|
||||
}
|
||||
}
|
||||
if placed == 0 {
|
||||
pokef(sc_x, 0, kx)
|
||||
poke32(sc_d, 0, kd)
|
||||
}
|
||||
}
|
||||
let wind = 0
|
||||
let oy = sy / TT_SS
|
||||
for i in 0 .. m - 1 {
|
||||
wind = wind + peek32(sc_d, i)
|
||||
if wind != 0 {
|
||||
let xa = peekf(sc_x, i)
|
||||
let xb = peekf(sc_x, i + 1)
|
||||
if xa < 0.0 { xa = 0.0 }
|
||||
if xb > fx(SW) { xb = fx(SW) }
|
||||
if xb > xa {
|
||||
let ixa = flr(xa)
|
||||
let ixb = flr(xb) + 1
|
||||
for sx in ixa .. ixb {
|
||||
let cxl = xa
|
||||
let cxr = xb
|
||||
if fx(sx) > cxl { cxl = fx(sx) }
|
||||
if fx(sx + 1) < cxr { cxr = fx(sx + 1) }
|
||||
let cvr = cxr - cxl
|
||||
if cvr > 0.0 {
|
||||
let oxp = sx / TT_SS
|
||||
if oxp >= 0 { if oxp < W { if oy >= 0 { if oy < H {
|
||||
let v = peek8(cover, oy * W + oxp) + flr(cvr * unit)
|
||||
if v > 255 { v = 255 }
|
||||
poke8(cover, oy * W + oxp, v)
|
||||
} } } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return cover
|
||||
}
|
||||
|
||||
# ---- glyph cache ----------------------------------------------------------
|
||||
# Rasterizing is far too slow to repeat per frame, so a coverage mask is kept
|
||||
# per (font, codepoint, size). Open addressing, 8 probes, evict on miss.
|
||||
fn gc_hash(font: int, cp: int, px: int) -> int {
|
||||
return band(bxor(bxor(cp * 2654435761, shl(font, 20)), shl(px, 8)), TT_GC - 1)
|
||||
}
|
||||
|
||||
fn tt_glyph_get(font: int, cp: int, px: int) -> int {
|
||||
let h = gc_hash(font, cp, px)
|
||||
for k in 0 .. 8 {
|
||||
let s = band(h + k, TT_GC - 1)
|
||||
if peek32(gc_used, s) == 1 {
|
||||
if peek32(gc_font, s) == font {
|
||||
if peek32(gc_cp, s) == cp {
|
||||
if peek32(gc_px, s) == px { return s }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let s = h
|
||||
if peek32(gc_used, s) == 1 {
|
||||
let old = peekp(gc_bmp, s)
|
||||
if ptr_is_null(old) == false { mem_free(old) }
|
||||
}
|
||||
let bmp = tt_raster(font, tt_glyph_index(font, cp), px)
|
||||
pokep(gc_bmp, s, bmp)
|
||||
poke32(gc_used, s, 1)
|
||||
poke32(gc_font, s, font)
|
||||
poke32(gc_cp, s, cp)
|
||||
poke32(gc_px, s, px)
|
||||
poke32(gc_w, s, gr_w)
|
||||
poke32(gc_h, s, gr_h)
|
||||
poke32(gc_ox, s, gr_ox)
|
||||
poke32(gc_oy, s, gr_oy)
|
||||
poke32(gc_adv, s, gr_adv)
|
||||
return s
|
||||
}
|
||||
|
||||
# ---- UTF-8 ----------------------------------------------------------------
|
||||
var u8_next: int = 0 # byte index just past the codepoint last decoded
|
||||
|
||||
fn tt_utf8(s: str, at: int) -> int {
|
||||
let c = peek8(s, at)
|
||||
let extra = 0 - 1
|
||||
if c < 128 { extra = 0 }
|
||||
if shr(c, 5) == 6 { extra = 1 }
|
||||
if shr(c, 4) == 14 { extra = 2 }
|
||||
if shr(c, 3) == 30 { extra = 3 }
|
||||
if extra < 0 {
|
||||
u8_next = at + 1
|
||||
return 65533
|
||||
}
|
||||
if extra == 0 {
|
||||
u8_next = at + 1
|
||||
return c
|
||||
}
|
||||
let cp = band(c, shr(63, extra))
|
||||
for i in 0 .. extra {
|
||||
let b = peek8(s, at + 1 + i)
|
||||
if band(b, 192) != 128 {
|
||||
u8_next = at + 1 + i
|
||||
return 65533
|
||||
}
|
||||
cp = bor(shl(cp, 6), band(b, 63))
|
||||
}
|
||||
u8_next = at + 1 + extra
|
||||
return cp
|
||||
}
|
||||
|
||||
# ---- drawing --------------------------------------------------------------
|
||||
fn tt_blit(bmp: ptr, w: int, h: int, dx: int, dy: int, colour: int) -> void {
|
||||
if ptr_is_null(bmp) { return }
|
||||
let cr = band(shr(colour, 16), 255)
|
||||
let cg = band(shr(colour, 8), 255)
|
||||
let cb = band(colour, 255)
|
||||
for y in 0 .. h {
|
||||
let py = dy + y
|
||||
if py >= 0 {
|
||||
if py < rt_fbh {
|
||||
for x in 0 .. w {
|
||||
let a = peek8(bmp, y * w + x)
|
||||
if a != 0 {
|
||||
let pxx = dx + x
|
||||
if pxx >= 0 {
|
||||
if pxx < rt_fbw {
|
||||
let d = peek32(rt_fb, py * rt_fbw + pxx)
|
||||
let rr = (cr * a + band(shr(d, 16), 255) * (255 - a)) / 255
|
||||
let rg = (cg * a + band(shr(d, 8), 255) * (255 - a)) / 255
|
||||
let rb = (cb * a + band(d, 255) * (255 - a)) / 255
|
||||
poke32(rt_fb, py * rt_fbw + pxx, bor(bor(shl(rr, 16), shl(rg, 8)), rb))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rt_text_ttf(font: int, x: int, y: int, s: str, colour: int, px: int) -> void {
|
||||
if font < 0 { return }
|
||||
if font >= tt_n { return }
|
||||
if px <= 0 { return }
|
||||
let upem = peek32(tt_upem, font)
|
||||
let scale = fx(px) / upem
|
||||
let baseline = y + flr(fx(peek32(tt_asc, font)) * scale + 0.5)
|
||||
let lineh = flr(fx(peek32(tt_asc, font) - peek32(tt_desc, font) + peek32(tt_gap, font)) * scale + 0.5)
|
||||
let penx = x
|
||||
let i = 0
|
||||
while peek8(s, i) != 0 {
|
||||
let cp = tt_utf8(s, i)
|
||||
i = u8_next
|
||||
if cp == 10 {
|
||||
penx = x
|
||||
baseline = baseline + lineh
|
||||
} else {
|
||||
let g = tt_glyph_get(font, cp, px)
|
||||
tt_blit(peekp(gc_bmp, g), peek32(gc_w, g), peek32(gc_h, g), penx + peek32(gc_ox, g), baseline - peek32(gc_oy, g), colour)
|
||||
penx = penx + peek32(gc_adv, g)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rt_text_w(font: int, s: str, px: int) -> int {
|
||||
if font < 0 { return 0 }
|
||||
if font >= tt_n { return 0 }
|
||||
if px <= 0 { return 0 }
|
||||
let w = 0
|
||||
let best = 0
|
||||
let i = 0
|
||||
while peek8(s, i) != 0 {
|
||||
let cp = tt_utf8(s, i)
|
||||
i = u8_next
|
||||
if cp == 10 {
|
||||
if w > best { best = w }
|
||||
w = 0
|
||||
} else {
|
||||
w = w + peek32(gc_adv, tt_glyph_get(font, cp, px))
|
||||
}
|
||||
}
|
||||
if w > best { best = w }
|
||||
return best
|
||||
}
|
||||
|
||||
fn rt_text_h(font: int, px: int) -> int {
|
||||
if font < 0 { return 0 }
|
||||
if font >= tt_n { return 0 }
|
||||
let scale = fx(px) / peek32(tt_upem, font)
|
||||
return flr(fx(peek32(tt_asc, font) - peek32(tt_desc, font) + peek32(tt_gap, font)) * scale + 0.5)
|
||||
}
|
||||
312
runtime/native/ui.ludic
Normal file
312
runtime/native/ui.ludic
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
# ============================================================================
|
||||
# ui.ludic — the retained UI engine, written in Ludic.
|
||||
#
|
||||
# A `ui Name { … }` block in a game is DATA: the compiler flattens the widget
|
||||
# tree and emits a build function that calls rt_ui_set() for each property.
|
||||
# Everything after that — measure/arrange layout, 9-slice skins, text, keyboard
|
||||
# focus and activation — is this file.
|
||||
#
|
||||
# Widgets are parallel arrays rather than a struct per widget, because Ludic
|
||||
# has components for per-entity data and raw memory for everything else, and a
|
||||
# widget is not an entity.
|
||||
# ============================================================================
|
||||
|
||||
const UI_MAX: int = 256
|
||||
|
||||
# widget types, matching the compiler's flattener
|
||||
const WT_PANEL: int = 0
|
||||
const WT_COL: int = 1
|
||||
const WT_ROW: int = 2
|
||||
const WT_LABEL: int = 3
|
||||
const WT_BUTTON: int = 4
|
||||
const WT_IMAGE: int = 5
|
||||
const WT_SPACER: int = 6
|
||||
|
||||
# property keys — the private protocol between ludicc and this runtime
|
||||
const K_TYPE: int = 0
|
||||
const K_PARENT: int = 1
|
||||
const K_W: int = 2
|
||||
const K_H: int = 3
|
||||
const K_X: int = 4
|
||||
const K_Y: int = 5
|
||||
const K_HASPOS: int = 6
|
||||
const K_PAD: int = 7
|
||||
const K_GAP: int = 8
|
||||
const K_BG: int = 9
|
||||
const K_FG: int = 10
|
||||
const K_BORDER: int = 11
|
||||
const K_ALIGN: int = 12
|
||||
const K_GROW: int = 13
|
||||
const K_FONT: int = 14
|
||||
const K_SIZE: int = 15
|
||||
const K_SKININSET: int = 16
|
||||
const K_FOCUSABLE: int = 17
|
||||
const K_VISIBLE: int = 18
|
||||
const K_IMG: int = 19
|
||||
const K_SKIN: int = 20
|
||||
|
||||
var ui_type: ptr = ptr_null()
|
||||
var ui_parent: ptr = ptr_null()
|
||||
var ui_w: ptr = ptr_null()
|
||||
var ui_h: ptr = ptr_null()
|
||||
var ui_x: ptr = ptr_null()
|
||||
var ui_y: ptr = ptr_null()
|
||||
var ui_haspos: ptr = ptr_null()
|
||||
var ui_pad: ptr = ptr_null()
|
||||
var ui_gap: ptr = ptr_null()
|
||||
var ui_bg: ptr = ptr_null()
|
||||
var ui_fg: ptr = ptr_null()
|
||||
var ui_border: ptr = ptr_null()
|
||||
var ui_align: ptr = ptr_null()
|
||||
var ui_grow: ptr = ptr_null()
|
||||
var ui_font: ptr = ptr_null()
|
||||
var ui_size: ptr = ptr_null()
|
||||
var ui_skin: ptr = ptr_null()
|
||||
var ui_inset: ptr = ptr_null()
|
||||
var ui_img: ptr = ptr_null()
|
||||
var ui_focusable: ptr = ptr_null()
|
||||
var ui_rx: ptr = ptr_null()
|
||||
var ui_ry: ptr = ptr_null()
|
||||
var ui_rw: ptr = ptr_null()
|
||||
var ui_rh: ptr = ptr_null()
|
||||
var ui_visible: ptr = ptr_null()
|
||||
var ui_fired: ptr = ptr_null()
|
||||
var ui_hasdyn: ptr = ptr_null()
|
||||
var ui_dyn: ptr = ptr_null() # UI_MAX * 96 bytes
|
||||
var ui_text: ptr = ptr_null() # UI_MAX static string pointers
|
||||
|
||||
var ui_n: int = 0
|
||||
var ui_active: int = -1
|
||||
var ui_focus: int = -1
|
||||
|
||||
fn rt_ui_init() -> void {
|
||||
ui_type = mem_alloc(UI_MAX * 4)
|
||||
ui_parent = mem_alloc(UI_MAX * 4)
|
||||
ui_w = mem_alloc(UI_MAX * 4)
|
||||
ui_h = mem_alloc(UI_MAX * 4)
|
||||
ui_x = mem_alloc(UI_MAX * 4)
|
||||
ui_y = mem_alloc(UI_MAX * 4)
|
||||
ui_haspos = mem_alloc(UI_MAX * 4)
|
||||
ui_pad = mem_alloc(UI_MAX * 4)
|
||||
ui_gap = mem_alloc(UI_MAX * 4)
|
||||
ui_bg = mem_alloc(UI_MAX * 4)
|
||||
ui_fg = mem_alloc(UI_MAX * 4)
|
||||
ui_border = mem_alloc(UI_MAX * 4)
|
||||
ui_align = mem_alloc(UI_MAX * 4)
|
||||
ui_grow = mem_alloc(UI_MAX * 4)
|
||||
ui_font = mem_alloc(UI_MAX * 4)
|
||||
ui_size = mem_alloc(UI_MAX * 4)
|
||||
ui_skin = mem_alloc(UI_MAX * 4)
|
||||
ui_inset = mem_alloc(UI_MAX * 4)
|
||||
ui_img = mem_alloc(UI_MAX * 4)
|
||||
ui_focusable = mem_alloc(UI_MAX * 4)
|
||||
ui_rx = mem_alloc(UI_MAX * 4)
|
||||
ui_ry = mem_alloc(UI_MAX * 4)
|
||||
ui_rw = mem_alloc(UI_MAX * 4)
|
||||
ui_rh = mem_alloc(UI_MAX * 4)
|
||||
ui_visible = mem_alloc(UI_MAX * 4)
|
||||
ui_fired = mem_alloc(UI_MAX * 4)
|
||||
ui_hasdyn = mem_alloc(UI_MAX * 4)
|
||||
ui_dyn = mem_alloc(UI_MAX * 96)
|
||||
ui_text = mem_alloc(UI_MAX * 8)
|
||||
}
|
||||
|
||||
# ---- build-time interface (called by compiler-emitted code) ---------------
|
||||
fn rt_ui_reset(n: int) -> void {
|
||||
ui_n = n
|
||||
for i in 0 .. n {
|
||||
poke32(ui_type, i, 0)
|
||||
poke32(ui_parent, i, 0 - 1)
|
||||
poke32(ui_w, i, 0)
|
||||
poke32(ui_h, i, 0)
|
||||
poke32(ui_x, i, 0)
|
||||
poke32(ui_y, i, 0)
|
||||
poke32(ui_haspos, i, 0)
|
||||
poke32(ui_pad, i, 0)
|
||||
poke32(ui_gap, i, 0)
|
||||
poke32(ui_bg, i, 0 - 1)
|
||||
poke32(ui_fg, i, 0 - 1)
|
||||
poke32(ui_border, i, 0 - 1)
|
||||
poke32(ui_align, i, 0 - 1)
|
||||
poke32(ui_grow, i, 0)
|
||||
poke32(ui_font, i, 0 - 1)
|
||||
poke32(ui_size, i, 8)
|
||||
poke32(ui_skin, i, 0 - 1)
|
||||
poke32(ui_inset, i, 6)
|
||||
poke32(ui_img, i, 0 - 1)
|
||||
poke32(ui_focusable, i, 0)
|
||||
poke32(ui_visible, i, 1)
|
||||
poke32(ui_fired, i, 0)
|
||||
poke32(ui_hasdyn, i, 0)
|
||||
pokep(ui_text, i, ptr_null())
|
||||
}
|
||||
}
|
||||
|
||||
fn rt_ui_set(i: int, k: int, v: int) -> void {
|
||||
if i < 0 { return }
|
||||
if i >= UI_MAX { return }
|
||||
if k == K_TYPE { poke32(ui_type, i, v) }
|
||||
if k == K_PARENT { poke32(ui_parent, i, v) }
|
||||
if k == K_W { poke32(ui_w, i, v) }
|
||||
if k == K_H { poke32(ui_h, i, v) }
|
||||
if k == K_X { poke32(ui_x, i, v) }
|
||||
if k == K_Y { poke32(ui_y, i, v) }
|
||||
if k == K_HASPOS { poke32(ui_haspos, i, v) }
|
||||
if k == K_PAD { poke32(ui_pad, i, v) }
|
||||
if k == K_GAP { poke32(ui_gap, i, v) }
|
||||
if k == K_BG { poke32(ui_bg, i, v) }
|
||||
if k == K_FG { poke32(ui_fg, i, v) }
|
||||
if k == K_BORDER { poke32(ui_border, i, v) }
|
||||
if k == K_ALIGN { poke32(ui_align, i, v) }
|
||||
if k == K_GROW { poke32(ui_grow, i, v) }
|
||||
if k == K_FONT { poke32(ui_font, i, v) }
|
||||
if k == K_SIZE { poke32(ui_size, i, v) }
|
||||
if k == K_SKININSET { poke32(ui_inset, i, v) }
|
||||
if k == K_FOCUSABLE { poke32(ui_focusable, i, v) }
|
||||
if k == K_VISIBLE { poke32(ui_visible, i, v) }
|
||||
if k == K_IMG { poke32(ui_img, i, v) }
|
||||
if k == K_SKIN { poke32(ui_skin, i, v) }
|
||||
}
|
||||
|
||||
fn rt_ui_static_text(i: int, s: str) -> void {
|
||||
if i < 0 { return }
|
||||
if i >= UI_MAX { return }
|
||||
pokep(ui_text, i, s)
|
||||
}
|
||||
|
||||
# ---- text helpers: TrueType when a font is loaded, bitmap otherwise -------
|
||||
fn ui_str(i: int) -> ptr {
|
||||
if peek32(ui_hasdyn, i) == 1 { return ptr_add(ui_dyn, i * 96) }
|
||||
let t = peekp(ui_text, i)
|
||||
if ptr_is_null(t) { return ptr_add(ui_dyn, i * 96) }
|
||||
return t
|
||||
}
|
||||
|
||||
fn ui_draw_text(font: int, x: int, y: int, s: ptr, colour: int, size: int) -> void {
|
||||
if font >= 0 {
|
||||
if font < tt_n {
|
||||
rt_text_ttf(font, x, y, s, colour, size)
|
||||
return
|
||||
}
|
||||
}
|
||||
rt_text(x, y, s, colour, max(size / 8, 1))
|
||||
}
|
||||
|
||||
fn ui_tw(font: int, s: ptr, size: int) -> int {
|
||||
if font >= 0 {
|
||||
if font < tt_n { return rt_text_w(font, s, size) }
|
||||
}
|
||||
let n = 0
|
||||
while peek8(s, n) != 0 { n = n + 1 }
|
||||
return n * 6 * max(size / 8, 1)
|
||||
}
|
||||
|
||||
fn ui_th(font: int, size: int) -> int {
|
||||
if font >= 0 {
|
||||
if font < tt_n { return rt_text_h(font, size) }
|
||||
}
|
||||
return 7 * max(size / 8, 1)
|
||||
}
|
||||
|
||||
fn ui_is_container(t: int) -> bool {
|
||||
if t == WT_PANEL { return true }
|
||||
if t == WT_COL { return true }
|
||||
if t == WT_ROW { return true }
|
||||
return false
|
||||
}
|
||||
fn ui_dir(t: int) -> int {
|
||||
if t == WT_ROW { return 0 }
|
||||
return 1
|
||||
}
|
||||
fn ui_under(i: int, root: int) -> bool {
|
||||
let k = i
|
||||
while k >= 0 {
|
||||
if k == root { return true }
|
||||
k = peek32(ui_parent, k)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
# ---- measure: content sizes, children before parents ----------------------
|
||||
fn ui_measure() -> void {
|
||||
let i = ui_n - 1
|
||||
while i >= 0 {
|
||||
if peek32(ui_visible, i) == 0 {
|
||||
poke32(ui_rw, i, 0)
|
||||
poke32(ui_rh, i, 0)
|
||||
} else {
|
||||
let t = peek32(ui_type, i)
|
||||
let pad = peek32(ui_pad, i)
|
||||
let cw = 0
|
||||
let ch = 0
|
||||
if t == WT_LABEL { cw = 0 - 1 }
|
||||
if t == WT_BUTTON { cw = 0 - 1 }
|
||||
if cw == 0 - 1 {
|
||||
let s = ui_str(i)
|
||||
cw = ui_tw(peek32(ui_font, i), s, peek32(ui_size, i)) + 2 * pad
|
||||
ch = ui_th(peek32(ui_font, i), peek32(ui_size, i)) + 2 * pad
|
||||
if t == WT_BUTTON {
|
||||
cw = cw + 10
|
||||
ch = ch + 6
|
||||
}
|
||||
} else {
|
||||
if t == WT_IMAGE {
|
||||
let im = peek32(ui_img, i)
|
||||
if im >= 0 {
|
||||
cw = peek32(img_w, im)
|
||||
ch = peek32(img_h, im)
|
||||
}
|
||||
} else {
|
||||
if t == WT_SPACER {
|
||||
cw = peek32(ui_w, i)
|
||||
ch = peek32(ui_h, i)
|
||||
} else {
|
||||
let dir = ui_dir(t)
|
||||
let mainsz = 0
|
||||
let cross = 0
|
||||
let nc = 0
|
||||
for c in 0 .. ui_n {
|
||||
if peek32(ui_parent, c) == i {
|
||||
if peek32(ui_visible, c) == 1 {
|
||||
if dir == 1 {
|
||||
mainsz = mainsz + peek32(ui_rh, c)
|
||||
cross = max(cross, peek32(ui_rw, c))
|
||||
} else {
|
||||
mainsz = mainsz + peek32(ui_rw, c)
|
||||
cross = max(cross, peek32(ui_rh, c))
|
||||
}
|
||||
nc = nc + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
if nc > 1 { mainsz = mainsz + peek32(ui_gap, i) * (nc - 1) }
|
||||
mainsz = mainsz + 2 * pad
|
||||
cross = cross + 2 * pad
|
||||
if dir == 1 {
|
||||
cw = cross
|
||||
ch = mainsz
|
||||
} else {
|
||||
cw = mainsz
|
||||
ch = cross
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let rw = peek32(ui_w, i)
|
||||
let rh = peek32(ui_h, i)
|
||||
if rw == 0 { rw = cw }
|
||||
if rh == 0 { rh = ch }
|
||||
poke32(ui_rw, i, rw)
|
||||
poke32(ui_rh, i, rh)
|
||||
}
|
||||
i = i - 1
|
||||
}
|
||||
}
|
||||
|
||||
fn ui_eff_align(child: int, parent: int) -> int {
|
||||
if peek32(ui_align, child) >= 0 { return peek32(ui_align, child) }
|
||||
if peek32(ui_align, parent) >= 0 { return peek32(ui_align, parent) }
|
||||
return 0
|
||||
}
|
||||
|
||||
import "ui_draw.ludic"
|
||||
283
runtime/native/ui_draw.ludic
Normal file
283
runtime/native/ui_draw.ludic
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
# ui_draw.ludic — layout arrange, drawing, focus/activation and the game-facing
|
||||
# accessors. Split out of ui.ludic (the storage/build/measure half).
|
||||
|
||||
# ---- arrange: rects, parents before children ------------------------------
|
||||
fn ui_arrange(i: int, x: int, y: int) -> void {
|
||||
poke32(ui_rx, i, x)
|
||||
poke32(ui_ry, i, y)
|
||||
let t = peek32(ui_type, i)
|
||||
if ui_is_container(t) == false { return }
|
||||
let dir = ui_dir(t)
|
||||
let pad = peek32(ui_pad, i)
|
||||
let gap = peek32(ui_gap, i)
|
||||
let innerw = peek32(ui_rw, i) - 2 * pad
|
||||
let innerh = peek32(ui_rh, i) - 2 * pad
|
||||
let total = 0
|
||||
let ng = 0
|
||||
let grows = 0
|
||||
for c in 0 .. ui_n {
|
||||
if peek32(ui_parent, c) == i {
|
||||
if peek32(ui_visible, c) == 1 {
|
||||
if dir == 1 { total = total + peek32(ui_rh, c) } else { total = total + peek32(ui_rw, c) }
|
||||
ng = ng + 1
|
||||
grows = grows + peek32(ui_grow, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
if ng > 1 { total = total + gap * (ng - 1) }
|
||||
let avail = innerw
|
||||
if dir == 1 { avail = innerh }
|
||||
let extra = max(avail - total, 0)
|
||||
let cx = x + pad
|
||||
let cy = y + pad
|
||||
for c in 0 .. ui_n {
|
||||
if peek32(ui_parent, c) == i {
|
||||
if peek32(ui_visible, c) == 1 {
|
||||
let kw = peek32(ui_rw, c)
|
||||
let kh = peek32(ui_rh, c)
|
||||
if peek32(ui_grow, c) != 0 {
|
||||
if grows > 0 {
|
||||
if dir == 1 { kh = kh + extra / grows } else { kw = kw + extra / grows }
|
||||
}
|
||||
}
|
||||
let al = ui_eff_align(c, i)
|
||||
poke32(ui_rw, c, kw)
|
||||
poke32(ui_rh, c, kh)
|
||||
if dir == 1 {
|
||||
let ax = x + pad
|
||||
if al == 1 { ax = x + pad + (innerw - kw) / 2 }
|
||||
if al == 2 { ax = x + pad + (innerw - kw) }
|
||||
ui_arrange(c, ax, cy)
|
||||
cy = cy + kh + gap
|
||||
} else {
|
||||
let ay = y + pad
|
||||
if al == 1 { ay = y + pad + (innerh - kh) / 2 }
|
||||
if al == 2 { ay = y + pad + (innerh - kh) }
|
||||
ui_arrange(c, cx, ay)
|
||||
cx = cx + kw + gap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui_layout() -> void {
|
||||
if ui_active < 0 { return }
|
||||
ui_measure()
|
||||
let px = (rt_fbw - peek32(ui_rw, ui_active)) / 2
|
||||
let py = (rt_fbh - peek32(ui_rh, ui_active)) / 2
|
||||
if peek32(ui_haspos, ui_active) == 1 {
|
||||
px = peek32(ui_x, ui_active)
|
||||
py = peek32(ui_y, ui_active)
|
||||
}
|
||||
ui_arrange(ui_active, px, py)
|
||||
}
|
||||
|
||||
# ---- drawing --------------------------------------------------------------
|
||||
fn ui_lighten(c: int) -> int {
|
||||
let r = band(shr(c, 16), 255)
|
||||
let g = band(shr(c, 8), 255)
|
||||
let b = band(c, 255)
|
||||
r = r + (255 - r) * 4 / 10
|
||||
g = g + (255 - g) * 4 / 10
|
||||
b = b + (255 - b) * 4 / 10
|
||||
return bor(bor(shl(r, 16), shl(g, 8)), b)
|
||||
}
|
||||
|
||||
fn ui_draw_node(i: int) -> void {
|
||||
if peek32(ui_visible, i) == 0 { return }
|
||||
let t = peek32(ui_type, i)
|
||||
let rx = peek32(ui_rx, i)
|
||||
let ry = peek32(ui_ry, i)
|
||||
let rw = peek32(ui_rw, i)
|
||||
let rh = peek32(ui_rh, i)
|
||||
let skin = peek32(ui_skin, i)
|
||||
let focused = 0
|
||||
if i == ui_focus { focused = 1 }
|
||||
|
||||
if t == WT_PANEL {
|
||||
if skin >= 0 {
|
||||
rt_draw_9slice(skin, rx, ry, rw, rh, peek32(ui_inset, i))
|
||||
} else {
|
||||
if peek32(ui_bg, i) >= 0 { rt_fill_rect(rx, ry, rw, rh, peek32(ui_bg, i)) }
|
||||
if peek32(ui_border, i) >= 0 { rt_frame_rect(rx, ry, rw, rh, peek32(ui_border, i)) }
|
||||
}
|
||||
}
|
||||
if t == WT_BUTTON {
|
||||
let bg = peek32(ui_bg, i)
|
||||
if bg < 0 { bg = 0x2a2a3a }
|
||||
if focused == 1 { bg = ui_lighten(bg) }
|
||||
if skin >= 0 {
|
||||
rt_draw_9slice(skin, rx, ry, rw, rh, peek32(ui_inset, i))
|
||||
} else {
|
||||
rt_fill_rect(rx, ry, rw, rh, bg)
|
||||
}
|
||||
let bc = peek32(ui_border, i)
|
||||
if bc < 0 { bc = 0x555577 }
|
||||
if focused == 1 { bc = 0xffff00 }
|
||||
rt_frame_rect(rx, ry, rw, rh, bc)
|
||||
let s = ui_str(i)
|
||||
let fg = peek32(ui_fg, i)
|
||||
if fg < 0 { fg = 0xffffff }
|
||||
let font = peek32(ui_font, i)
|
||||
let size = peek32(ui_size, i)
|
||||
ui_draw_text(font, rx + (rw - ui_tw(font, s, size)) / 2, ry + (rh - ui_th(font, size)) / 2, s, fg, size)
|
||||
}
|
||||
if t == WT_LABEL {
|
||||
let s = ui_str(i)
|
||||
let fg = peek32(ui_fg, i)
|
||||
if fg < 0 { fg = 0xffffff }
|
||||
let font = peek32(ui_font, i)
|
||||
let size = peek32(ui_size, i)
|
||||
let pad = peek32(ui_pad, i)
|
||||
let tw = ui_tw(font, s, size)
|
||||
let tx = rx + pad
|
||||
let al = peek32(ui_align, i)
|
||||
if al == 1 { tx = rx + (rw - tw) / 2 }
|
||||
if al == 2 { tx = rx + rw - pad - tw }
|
||||
ui_draw_text(font, tx, ry + pad, s, fg, size)
|
||||
}
|
||||
if t == WT_IMAGE {
|
||||
if peek32(ui_img, i) >= 0 { rt_draw_image_scaled(peek32(ui_img, i), rx, ry, rw, rh) }
|
||||
}
|
||||
for c in 0 .. ui_n {
|
||||
if peek32(ui_parent, c) == i { ui_draw_node(c) }
|
||||
}
|
||||
}
|
||||
|
||||
# ---- focus and activation -------------------------------------------------
|
||||
var ui_fl: ptr = ptr_null()
|
||||
|
||||
fn ui_focusables() -> int {
|
||||
if ptr_is_null(ui_fl) { ui_fl = mem_alloc(UI_MAX * 4) }
|
||||
let n = 0
|
||||
for i in 0 .. ui_n {
|
||||
if peek32(ui_focusable, i) == 1 {
|
||||
if peek32(ui_visible, i) == 1 {
|
||||
if ui_under(i, ui_active) {
|
||||
poke32(ui_fl, n, i)
|
||||
n = n + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
fn rt_ui_open(root: int) -> void {
|
||||
ui_active = root
|
||||
let n = ui_focusables()
|
||||
ui_focus = 0 - 1
|
||||
if n > 0 { ui_focus = peek32(ui_fl, 0) }
|
||||
}
|
||||
|
||||
fn rt_ui_tick(k: int) -> void {
|
||||
if ui_active < 0 { return }
|
||||
for i in 0 .. ui_n { poke32(ui_fired, i, 0) }
|
||||
ui_layout()
|
||||
let n = ui_focusables()
|
||||
if n == 0 {
|
||||
ui_focus = 0 - 1
|
||||
return
|
||||
}
|
||||
let cur = 0 - 1
|
||||
for i in 0 .. n {
|
||||
if peek32(ui_fl, i) == ui_focus { cur = i }
|
||||
}
|
||||
if cur < 0 {
|
||||
cur = 0
|
||||
ui_focus = peek32(ui_fl, 0)
|
||||
}
|
||||
if k == 119 { # 'w'
|
||||
cur = (cur - 1 + n) % n
|
||||
ui_focus = peek32(ui_fl, cur)
|
||||
}
|
||||
if k == 115 { # 's'
|
||||
cur = (cur + 1) % n
|
||||
ui_focus = peek32(ui_fl, cur)
|
||||
}
|
||||
if k == 32 { poke32(ui_fired, ui_focus, 1) }
|
||||
if k == 10 { poke32(ui_fired, ui_focus, 1) }
|
||||
if k == 13 { poke32(ui_fired, ui_focus, 1) }
|
||||
}
|
||||
|
||||
fn rt_ui_render() -> void {
|
||||
if ui_active < 0 { return }
|
||||
ui_layout()
|
||||
ui_draw_node(ui_active)
|
||||
}
|
||||
|
||||
# ---- game-facing accessors ------------------------------------------------
|
||||
fn rt_ui_clicked(id: int) -> bool {
|
||||
if id < 0 { return false }
|
||||
if id >= ui_n { return false }
|
||||
return peek32(ui_fired, id) == 1
|
||||
}
|
||||
|
||||
fn rt_ui_set_text(id: int, s: str) -> void {
|
||||
if id < 0 { return }
|
||||
if id >= ui_n { return }
|
||||
let base = id * 96
|
||||
let i = 0
|
||||
let ch = peek8(s, 0)
|
||||
while ch != 0 {
|
||||
if i >= 95 { ch = 0 }
|
||||
if ch != 0 {
|
||||
poke8(ui_dyn, base + i, ch)
|
||||
i = i + 1
|
||||
ch = peek8(s, i)
|
||||
}
|
||||
}
|
||||
poke8(ui_dyn, base + i, 0)
|
||||
poke32(ui_hasdyn, id, 1)
|
||||
}
|
||||
|
||||
fn rt_ui_set_int(id: int, n: int) -> void {
|
||||
if id < 0 { return }
|
||||
if id >= ui_n { return }
|
||||
let base = id * 96
|
||||
let at = 0
|
||||
let v = n
|
||||
if v < 0 {
|
||||
poke8(ui_dyn, base, 45)
|
||||
at = 1
|
||||
v = 0 - v
|
||||
}
|
||||
if v == 0 {
|
||||
poke8(ui_dyn, base + at, 48)
|
||||
at = at + 1
|
||||
} else {
|
||||
let digits = 0
|
||||
let t = v
|
||||
while t > 0 {
|
||||
digits = digits + 1
|
||||
t = t / 10
|
||||
}
|
||||
let p = digits
|
||||
while p > 0 {
|
||||
let div = 1
|
||||
for k in 1 .. p { div = div * 10 }
|
||||
poke8(ui_dyn, base + at, 48 + (v / div) % 10)
|
||||
at = at + 1
|
||||
p = p - 1
|
||||
}
|
||||
}
|
||||
poke8(ui_dyn, base + at, 0)
|
||||
poke32(ui_hasdyn, id, 1)
|
||||
}
|
||||
|
||||
fn rt_ui_focus(id: int) -> void {
|
||||
if id < 0 { return }
|
||||
if id >= ui_n { return }
|
||||
ui_focus = id
|
||||
}
|
||||
|
||||
fn rt_ui_focused() -> int {
|
||||
return ui_focus
|
||||
}
|
||||
|
||||
fn rt_ui_visible(id: int, v: int) -> void {
|
||||
if id < 0 { return }
|
||||
if id >= ui_n { return }
|
||||
poke32(ui_visible, id, v)
|
||||
}
|
||||
32
runtime/web/imports.txt
Normal file
32
runtime/web/imports.txt
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# runtime/web/imports.txt — every symbol a web build may leave undefined.
|
||||
#
|
||||
# ludicc passes this to wasm-ld as --allow-undefined-file, so these become the
|
||||
# module's wasm imports and *anything else* undefined is a link error at build
|
||||
# time rather than a LinkError in the browser. That matters because LLVM invents
|
||||
# libcalls (calloc, bcmp, memmove) that ludicc never emitted: without this list
|
||||
# they would silently become imports nobody implements.
|
||||
#
|
||||
# runtime/web/platform.js supplies all of them.
|
||||
|
||||
# the window — the same five-function protocol runtime/native/cocoa.ll implements
|
||||
win_open
|
||||
win_poll
|
||||
win_present
|
||||
win_running
|
||||
win_close
|
||||
|
||||
# files: a preloaded image of the game's assets, plus localStorage for saves
|
||||
web_fopen
|
||||
web_fread
|
||||
web_fwrite
|
||||
web_fclose
|
||||
web_fseek
|
||||
web_ftell
|
||||
|
||||
# stdin, stdout, the clock, and giving up
|
||||
web_getchar
|
||||
web_putchar
|
||||
web_print_str
|
||||
web_print_int
|
||||
web_time
|
||||
web_exit
|
||||
106
runtime/web/index.html
Normal file
106
runtime/web/index.html
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
<!doctype html>
|
||||
<!-- =========================================================================
|
||||
runtime/web/index.html — the page a web build is served from.
|
||||
|
||||
ludicc copies this file next to the .wasm, substituting the module name and
|
||||
the game's title. It mounts a canvas, hands it to runtime/web/platform.js,
|
||||
and gets out of the way: everything you see after that is drawn by compiled
|
||||
Ludic writing pixels into its own framebuffer.
|
||||
========================================================================= -->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>{{TITLE}}</title>
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
* { box-sizing: border-box; }
|
||||
html, body {
|
||||
margin: 0; height: 100%;
|
||||
background: #101018;
|
||||
color: #c8c8d8;
|
||||
font: 14px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
body {
|
||||
display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center;
|
||||
gap: 14px; padding: 16px;
|
||||
}
|
||||
h1 { margin: 0; font-size: 15px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: #8a8aa8; }
|
||||
#screen {
|
||||
display: block;
|
||||
background: #000;
|
||||
border: 1px solid #2a2a3c;
|
||||
border-radius: 3px;
|
||||
/* platform.js publishes --ludic-w/h/scale when the game opens its window.
|
||||
Draw at the scale it asked for, but never wider than the page or taller
|
||||
than the room left over — and derive the limit from the *width* so the
|
||||
aspect ratio survives, which a plain max-height would not do. */
|
||||
--fit-height: 72dvh;
|
||||
width: min(
|
||||
calc(var(--ludic-w, 320) * var(--ludic-scale, 1) * 1px),
|
||||
100%,
|
||||
calc(var(--fit-height) * var(--ludic-w, 320) / var(--ludic-h, 240))
|
||||
);
|
||||
image-rendering: pixelated;
|
||||
touch-action: none;
|
||||
}
|
||||
#status { min-height: 1.5em; color: #6a6a86; font-size: 12px; text-align: center; }
|
||||
#status.error { color: #d08a8a; }
|
||||
/* An on-screen d-pad, shown only where there is no keyboard to type on. */
|
||||
#pad { display: none; gap: 8px; }
|
||||
#pad button {
|
||||
width: 52px; height: 52px;
|
||||
font: inherit; font-size: 16px;
|
||||
color: #c8c8d8; background: #1c1c2a;
|
||||
border: 1px solid #33334a; border-radius: 6px;
|
||||
}
|
||||
#pad button:active { background: #2a2a3e; }
|
||||
@media (hover: none) and (pointer: coarse) { #pad { display: grid; grid-template-columns: repeat(3, auto); } }
|
||||
.spacer { visibility: hidden; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>{{TITLE}}</h1>
|
||||
<canvas id="screen" width="320" height="240"></canvas>
|
||||
<p id="status">loading…</p>
|
||||
|
||||
<div id="pad" aria-label="on-screen controls">
|
||||
<button class="spacer" tabindex="-1"></button>
|
||||
<button data-key="w" aria-label="up">↑</button>
|
||||
<button class="spacer" tabindex="-1"></button>
|
||||
<button data-key="a" aria-label="left">←</button>
|
||||
<button data-key=" " aria-label="confirm">•</button>
|
||||
<button data-key="d" aria-label="right">→</button>
|
||||
<button class="spacer" tabindex="-1"></button>
|
||||
<button data-key="s" aria-label="down">↓</button>
|
||||
<button class="spacer" tabindex="-1"></button>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { LudicWeb } from './platform.js';
|
||||
|
||||
const status = document.getElementById('status');
|
||||
|
||||
try {
|
||||
const game = await LudicWeb.start({
|
||||
wasm: '{{WASM}}',
|
||||
canvas: document.getElementById('screen'),
|
||||
manifest: 'assets.json',
|
||||
onExit: () => { status.textContent = 'game over — reload to play again'; },
|
||||
});
|
||||
status.textContent = 'running';
|
||||
|
||||
for (const b of document.querySelectorAll('#pad button[data-key]')) {
|
||||
const send = (ev) => { ev.preventDefault(); game.press(b.dataset.key); };
|
||||
b.addEventListener('pointerdown', send);
|
||||
}
|
||||
} catch (err) {
|
||||
status.className = 'error';
|
||||
status.textContent = 'failed to start: ' + err.message;
|
||||
throw err;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
357
runtime/web/platform.js
Normal file
357
runtime/web/platform.js
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
// ============================================================================
|
||||
// runtime/web/platform.js — the web's window, keyboard and filesystem.
|
||||
//
|
||||
// This file is the browser's answer to runtime/native/cocoa.ll, and it has the
|
||||
// same job and the same size: implement the five-function platform protocol
|
||||
//
|
||||
// win_open(w, h, scale, title) win_poll() -> key win_present(fb, w, h)
|
||||
// win_running() -> bool win_close()
|
||||
//
|
||||
// plus the handful of host services wasm has no OS to ask for — files, the
|
||||
// clock, stdout. Nothing else. The game, the ECS, the framebuffer, the PNG
|
||||
// decoder, the TrueType rasteriser and the UI are all compiled Ludic running
|
||||
// inside the wasm module; no game logic passes through this file, and none of
|
||||
// it is interpreted.
|
||||
//
|
||||
// const game = await LudicWeb.start({ wasm: 'chronorift.wasm',
|
||||
// canvas: document.querySelector('canvas'),
|
||||
// manifest: 'assets.json' })
|
||||
// ============================================================================
|
||||
|
||||
export const LudicWeb = { start };
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Keyboard. win_poll() reports the last key pressed since the previous frame,
|
||||
// as a character code, or 0 for none — exactly what cocoa.ll reports. Arrow
|
||||
// keys fold onto WASD and Escape onto 'q', so a game written for the native
|
||||
// build needs no web-specific input handling.
|
||||
// ---------------------------------------------------------------------------
|
||||
const KEY_ALIASES = {
|
||||
ArrowUp: 'w', ArrowLeft: 'a', ArrowDown: 's', ArrowRight: 'd',
|
||||
Escape: 'q', Enter: '\r', Backspace: '\b', Tab: '\t',
|
||||
};
|
||||
|
||||
function keyCode(ev) {
|
||||
const alias = KEY_ALIASES[ev.key];
|
||||
if (alias) return alias.charCodeAt(0);
|
||||
if (ev.key.length === 1) return ev.key.charCodeAt(0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// A read-only image of the game's files, plus a writable overlay in
|
||||
// localStorage for save games.
|
||||
//
|
||||
// The Ludic runtime opens assets with file_open()/file_read(), which on a
|
||||
// native build is fopen/fread. The browser has no synchronous file access, so
|
||||
// every file the game can open is fetched up front (see the manifest ludicc
|
||||
// emits beside the .wasm) and served out of memory. Saves go the other way:
|
||||
// file_open("game.sav","wb") buffers, and fclose commits to localStorage, so
|
||||
// save/load survives a page reload.
|
||||
// ---------------------------------------------------------------------------
|
||||
export class Files {
|
||||
constructor(preloaded, storageKey) {
|
||||
this.assets = preloaded; // Map<string, Uint8Array>
|
||||
this.storageKey = storageKey;
|
||||
this.open = new Map(); // handle -> {data, pos, path, write}
|
||||
this.next = 1; // 0 is NULL to the Ludic side
|
||||
}
|
||||
|
||||
saved(path) {
|
||||
try {
|
||||
const raw = localStorage.getItem(this.storageKey + path);
|
||||
if (raw === null) return null;
|
||||
const bytes = new Uint8Array(raw.length);
|
||||
for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i) & 0xff;
|
||||
return bytes;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
commit(path, data) {
|
||||
try {
|
||||
let raw = '';
|
||||
for (let i = 0; i < data.length; i += 0x8000)
|
||||
raw += String.fromCharCode.apply(null, data.subarray(i, i + 0x8000));
|
||||
localStorage.setItem(this.storageKey + path, raw);
|
||||
} catch (e) {
|
||||
console.warn('ludic: could not persist', path, e);
|
||||
}
|
||||
}
|
||||
|
||||
fopen(path, mode) {
|
||||
const writing = mode.includes('w') || mode.includes('a');
|
||||
if (writing) {
|
||||
const h = this.next++;
|
||||
this.open.set(h, { path, pos: 0, write: true, chunks: [], size: 0 });
|
||||
return h;
|
||||
}
|
||||
// Reads prefer a save the player has made over the shipped asset.
|
||||
const data = this.saved(path) || this.assets.get(path) ||
|
||||
this.assets.get(path.replace(/^\.\//, ''));
|
||||
if (!data) return 0; // NULL: the game handles it
|
||||
const h = this.next++;
|
||||
this.open.set(h, { path, pos: 0, write: false, data });
|
||||
return h;
|
||||
}
|
||||
|
||||
fread(h, dst, want) {
|
||||
const f = this.open.get(h);
|
||||
if (!f || f.write) return 0;
|
||||
const n = Math.max(0, Math.min(want, f.data.length - f.pos));
|
||||
dst.set(f.data.subarray(f.pos, f.pos + n));
|
||||
f.pos += n;
|
||||
return n;
|
||||
}
|
||||
|
||||
fwrite(h, src, n) {
|
||||
const f = this.open.get(h);
|
||||
if (!f || !f.write) return 0;
|
||||
f.chunks.push(src.slice(0, n)); // copy: wasm memory is reused
|
||||
f.size += n;
|
||||
f.pos += n;
|
||||
return n;
|
||||
}
|
||||
|
||||
fclose(h) {
|
||||
const f = this.open.get(h);
|
||||
if (!f) return;
|
||||
if (f.write) {
|
||||
const all = new Uint8Array(f.size);
|
||||
let at = 0;
|
||||
for (const c of f.chunks) { all.set(c, at); at += c.length; }
|
||||
this.commit(f.path, all);
|
||||
}
|
||||
this.open.delete(h);
|
||||
}
|
||||
|
||||
fseek(h, off, whence) {
|
||||
const f = this.open.get(h);
|
||||
if (!f) return -1;
|
||||
const end = f.write ? f.size : f.data.length;
|
||||
const base = whence === 1 ? f.pos : whence === 2 ? end : 0;
|
||||
f.pos = Math.max(0, Math.min(end, base + off));
|
||||
return 0;
|
||||
}
|
||||
|
||||
ftell(h) {
|
||||
const f = this.open.get(h);
|
||||
return f ? f.pos : -1;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The canvas. win_present() hands over a pointer to the framebuffer — one
|
||||
// 0x00RRGGBB word per pixel — which is transcoded into the RGBA byte order
|
||||
// ImageData wants and blitted at the requested integer scale.
|
||||
// ---------------------------------------------------------------------------
|
||||
class Screen {
|
||||
constructor(canvas) {
|
||||
this.canvas = canvas;
|
||||
this.ctx = null;
|
||||
this.image = null;
|
||||
this.w = 0; this.h = 0;
|
||||
}
|
||||
|
||||
open(w, h, scale, title) {
|
||||
this.w = w; this.h = h;
|
||||
this.canvas.width = w;
|
||||
this.canvas.height = h;
|
||||
// The backing store stays at the framebuffer's own resolution and CSS does
|
||||
// the scaling, so a 320x240 game stays crisp on a HiDPI display.
|
||||
//
|
||||
// How big to draw it is the page's decision, not the platform layer's: a 3x
|
||||
// 320x240 window is 720px tall and does not fit a laptop viewport once there
|
||||
// is a title above it. So this publishes the framebuffer's dimensions and
|
||||
// the scale the game asked for, and leaves the fitting to CSS — see the
|
||||
// width rule in runtime/web/index.html.
|
||||
const css = this.canvas.style;
|
||||
css.setProperty('--ludic-w', String(w));
|
||||
css.setProperty('--ludic-h', String(h));
|
||||
css.setProperty('--ludic-scale', String(scale));
|
||||
css.aspectRatio = w + ' / ' + h;
|
||||
css.height = 'auto';
|
||||
css.imageRendering = 'pixelated';
|
||||
this.ctx = this.canvas.getContext('2d', { alpha: false });
|
||||
this.ctx.imageSmoothingEnabled = false;
|
||||
this.image = this.ctx.createImageData(w, h);
|
||||
this.rgba = new Uint32Array(this.image.data.buffer);
|
||||
if (title) document.title = title;
|
||||
}
|
||||
|
||||
present(words, w, h) {
|
||||
if (!this.ctx) return;
|
||||
if (w !== this.w || h !== this.h) this.open(w, h, 1, null);
|
||||
const dst = this.rgba;
|
||||
// Little-endian ImageData is 0xAABBGGRR; the framebuffer is 0x00RRGGBB.
|
||||
for (let i = 0, n = w * h; i < n; i++) {
|
||||
const c = words[i];
|
||||
dst[i] = 0xff000000 | ((c & 0xff) << 16) | (c & 0xff00) | ((c >>> 16) & 0xff);
|
||||
}
|
||||
this.ctx.putImageData(this.image, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Host — the memory views and the services wasm has no OS to ask for.
|
||||
//
|
||||
// A wasm module's linear memory is detached and replaced every time it grows,
|
||||
// so every view has to be re-derived rather than cached across a call. Beyond
|
||||
// that this is just the non-window half of the platform: files, stdin, stdout
|
||||
// and the clock. The browser and Node need exactly the same set, so the Node
|
||||
// test runner (tools/ludic-web/run.mjs) builds on this class too, and only the
|
||||
// five win_* functions differ between the two hosts.
|
||||
// ---------------------------------------------------------------------------
|
||||
export class Host {
|
||||
constructor({ files, stdin = '', log = console.log } = {}) {
|
||||
this.files = files;
|
||||
this.stdin = stdin;
|
||||
this.stdinAt = 0;
|
||||
this.log = log;
|
||||
this.line = '';
|
||||
this.memory = null;
|
||||
this.buffer = null;
|
||||
this.running = 1;
|
||||
this.exitCode = null;
|
||||
}
|
||||
|
||||
bind(memory) { this.memory = memory; this.sync(); }
|
||||
|
||||
sync() {
|
||||
if (this.memory.buffer !== this.buffer) {
|
||||
this.buffer = this.memory.buffer;
|
||||
this.u8 = new Uint8Array(this.buffer);
|
||||
this.u32 = new Uint32Array(this.buffer);
|
||||
}
|
||||
}
|
||||
|
||||
cstr(p) {
|
||||
this.sync();
|
||||
let end = p;
|
||||
while (this.u8[end] !== 0) end++;
|
||||
return new TextDecoder().decode(this.u8.subarray(p, end));
|
||||
}
|
||||
|
||||
flush() { if (this.line) { this.log(this.line); this.line = ''; } }
|
||||
|
||||
imports() {
|
||||
const fs = this.files;
|
||||
return {
|
||||
web_fopen: (path, mode) => fs.fopen(this.cstr(path), this.cstr(mode)),
|
||||
web_fread: (h, dst, n) => { this.sync(); return fs.fread(h, this.u8.subarray(dst, dst + n), n); },
|
||||
web_fwrite: (h, src, n) => { this.sync(); return fs.fwrite(h, this.u8.subarray(src, src + n), n); },
|
||||
web_fclose: (h) => fs.fclose(h),
|
||||
web_fseek: (h, off, whence) => fs.fseek(h, off, whence),
|
||||
web_ftell: (h) => fs.ftell(h),
|
||||
|
||||
web_getchar: () => (this.stdinAt < this.stdin.length ? this.stdin.charCodeAt(this.stdinAt++) : -1),
|
||||
web_putchar: (c) => { if (c === 10) this.flush(); else this.line += String.fromCharCode(c); },
|
||||
web_print_str: (p) => { this.line += this.cstr(p); },
|
||||
// print_int(n) is printf("%d\n", n) natively, newline included.
|
||||
web_print_int: (n) => { this.line += String(n); this.flush(); },
|
||||
web_time: () => Math.floor(Date.now() / 1000),
|
||||
web_exit: (code) => {
|
||||
this.running = 0; this.exitCode = code; this.flush();
|
||||
throw new ExitSignal(code);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// start() — fetch the module and its assets, wire the imports, run the loop.
|
||||
//
|
||||
// The native entry point is `while (alive) frame();`. A browser tab cannot be
|
||||
// held that way, so a web build exports the loop's parts instead —
|
||||
// ludic_boot / ludic_frame / ludic_alive / ludic_teardown — and this drives
|
||||
// them from requestAnimationFrame. The systems that run per frame, and their
|
||||
// order, are identical; only who owns the loop changes.
|
||||
// ---------------------------------------------------------------------------
|
||||
async function start({ wasm, canvas, manifest = null, files = {}, onExit = null,
|
||||
stdin = '', storageKey = 'ludic:' } = {}) {
|
||||
const assets = new Map();
|
||||
for (const [path, bytes] of Object.entries(files)) assets.set(path, bytes);
|
||||
|
||||
if (manifest) {
|
||||
const list = await (await fetch(manifest)).json();
|
||||
await Promise.all(list.map(async (path) => {
|
||||
const res = await fetch(path);
|
||||
if (!res.ok) { console.warn('ludic: missing asset', path); return; }
|
||||
assets.set(path, new Uint8Array(await res.arrayBuffer()));
|
||||
}));
|
||||
}
|
||||
|
||||
const host = new Host({ files: new Files(assets, storageKey) });
|
||||
const screen = new Screen(canvas);
|
||||
let pendingKey = 0;
|
||||
|
||||
const env = {
|
||||
...host.imports(),
|
||||
// The window, which is the only part of the platform the browser and the
|
||||
// test runner do not share.
|
||||
win_open: (w, h, scale, title) => screen.open(w, h, scale || 1, host.cstr(title)),
|
||||
win_poll: () => { const k = pendingKey; pendingKey = 0; return k; },
|
||||
win_present: (fb, w, h) => {
|
||||
host.sync();
|
||||
screen.present(host.u32.subarray(fb >>> 2, (fb >>> 2) + w * h), w, h);
|
||||
},
|
||||
win_running: () => host.running,
|
||||
win_close: () => { host.running = 0; },
|
||||
};
|
||||
|
||||
const { instance } = await WebAssembly.instantiateStreaming(fetch(wasm), { env });
|
||||
host.bind(instance.exports.memory);
|
||||
|
||||
const { ludic_boot, ludic_frame, ludic_alive, ludic_teardown } = instance.exports;
|
||||
|
||||
const onKey = (ev) => {
|
||||
const k = keyCode(ev);
|
||||
if (k) { pendingKey = k; ev.preventDefault(); }
|
||||
};
|
||||
window.addEventListener('keydown', onKey);
|
||||
|
||||
const game = {
|
||||
instance,
|
||||
exports: instance.exports,
|
||||
// Lets a page wire on-screen buttons for touch devices: game.press('w').
|
||||
press: (k) => { pendingKey = typeof k === 'string' ? k.charCodeAt(0) : k; },
|
||||
stop: () => { host.running = 0; },
|
||||
};
|
||||
|
||||
let stopped = false;
|
||||
const finish = () => {
|
||||
if (stopped) return;
|
||||
stopped = true;
|
||||
window.removeEventListener('keydown', onKey);
|
||||
try { ludic_teardown(); } catch (e) { if (!(e instanceof ExitSignal)) throw e; }
|
||||
host.flush();
|
||||
if (onExit) onExit(host.exitCode ?? 0);
|
||||
};
|
||||
|
||||
const step = () => {
|
||||
if (stopped) return;
|
||||
try {
|
||||
if (!host.running || !ludic_alive()) return finish();
|
||||
ludic_frame();
|
||||
} catch (e) {
|
||||
if (e instanceof ExitSignal) return finish();
|
||||
stopped = true;
|
||||
window.removeEventListener('keydown', onKey);
|
||||
throw e;
|
||||
}
|
||||
requestAnimationFrame(step);
|
||||
};
|
||||
|
||||
try { ludic_boot(); } catch (e) {
|
||||
if (!(e instanceof ExitSignal)) throw e;
|
||||
finish();
|
||||
return game;
|
||||
}
|
||||
requestAnimationFrame(step);
|
||||
return game;
|
||||
}
|
||||
|
||||
// os_exit() has to unwind the wasm frame it was called from; there is no
|
||||
// process to terminate, so it throws and the loop treats it as the end.
|
||||
export class ExitSignal extends Error {
|
||||
constructor(code) { super('ludic: exit(' + code + ')'); this.code = code; }
|
||||
}
|
||||
369
runtime/web/wasm.ll
Normal file
369
runtime/web/wasm.ll
Normal file
|
|
@ -0,0 +1,369 @@
|
|||
; ============================================================================
|
||||
; runtime/web/wasm.ll — the web platform layer, written in LLVM IR.
|
||||
;
|
||||
; This is to the browser what runtime/native/cocoa.ll is to macOS: the one
|
||||
; per-platform file, hand-written in the same IR ludicc emits, assembled by the
|
||||
; same toolchain and handed to the same linker. No C is compiled here either.
|
||||
;
|
||||
; It exists because wasm32-unknown-unknown has no libc. The Ludic runtime
|
||||
; (runtime/native/*.ludic) stands on a floor of ~15 platform calls — malloc,
|
||||
; memcpy, fopen, getchar — and on a native target those are the OS's. On the
|
||||
; web there is no OS underneath, so this file *is* the floor:
|
||||
;
|
||||
; * the allocator is implemented here, over wasm linear memory;
|
||||
; * memcpy/memset/strlen are implemented here;
|
||||
; * everything genuinely external — files, stdin, stdout, the clock, and the
|
||||
; five win_* window functions — is a wasm import, resolved by
|
||||
; runtime/web/platform.js.
|
||||
;
|
||||
; Nothing above this file changes for the web. core.ludic, image.ludic,
|
||||
; truetype.ludic and ui.ludic compile to wasm unmodified.
|
||||
; ============================================================================
|
||||
|
||||
target triple = "wasm32-unknown-unknown"
|
||||
|
||||
declare i32 @llvm.wasm.memory.size.i32(i32)
|
||||
declare i32 @llvm.wasm.memory.grow.i32(i32, i32)
|
||||
declare void @llvm.memcpy.p0.p0.i32(ptr, ptr, i32, i1)
|
||||
declare void @llvm.memmove.p0.p0.i32(ptr, ptr, i32, i1)
|
||||
declare void @llvm.memset.p0.i32(ptr, i8, i32, i1)
|
||||
|
||||
; wasm-ld places this symbol immediately after the static data segments; its
|
||||
; *address* is where our heap may begin.
|
||||
@__heap_base = external global i8
|
||||
|
||||
; ---------------------------------------------------------------------------
|
||||
; The allocator.
|
||||
;
|
||||
; A first-fit free list over linear memory, growing the wasm memory with
|
||||
; memory.grow when it runs out. Every block carries an 8-byte header
|
||||
; [i32 payload_size][i32 next_free] and the pointer handed out is header+8, so
|
||||
; free() is O(1) and malloc() reuses blocks of a size it has seen before —
|
||||
; which is the shape of the load the PNG decoder actually makes (a dozen tiles
|
||||
; of identical dimensions, each decode allocating and releasing the same three
|
||||
; buffers). Blocks are split when the fit is loose; adjacent free blocks are
|
||||
; not coalesced, which costs some fragmentation and buys a lot of simplicity.
|
||||
; ---------------------------------------------------------------------------
|
||||
|
||||
@ludic_brk = internal global i32 0 ; bump pointer; 0 until first use
|
||||
@ludic_free = internal global i32 0 ; head of the free list, 0 = empty
|
||||
|
||||
define ptr @malloc(i32 %n0) {
|
||||
entry:
|
||||
%small = icmp slt i32 %n0, 1
|
||||
%n1 = select i1 %small, i32 1, i32 %n0
|
||||
%pad = add i32 %n1, 7
|
||||
%n = and i32 %pad, -8 ; 8-byte aligned payloads
|
||||
%head = load i32, ptr @ludic_free
|
||||
br label %scan
|
||||
|
||||
scan: ; walk the free list, first fit
|
||||
%cur = phi i32 [ %head, %entry ], [ %nxt, %scan.next ]
|
||||
%prev = phi i32 [ 0, %entry ], [ %cur, %scan.next ]
|
||||
%end = icmp eq i32 %cur, 0
|
||||
br i1 %end, label %bump, label %check
|
||||
|
||||
check:
|
||||
%cp = inttoptr i32 %cur to ptr
|
||||
%sz = load i32, ptr %cp
|
||||
%nxtp = getelementptr i8, ptr %cp, i32 4
|
||||
%nxt = load i32, ptr %nxtp
|
||||
%fits = icmp sge i32 %sz, %n
|
||||
br i1 %fits, label %take, label %scan.next
|
||||
|
||||
scan.next:
|
||||
br label %scan
|
||||
|
||||
take: ; unlink %cur from the free list
|
||||
%first = icmp eq i32 %prev, 0
|
||||
br i1 %first, label %unlink.head, label %unlink.mid
|
||||
unlink.head:
|
||||
store i32 %nxt, ptr @ludic_free
|
||||
br label %split
|
||||
unlink.mid:
|
||||
%pp = inttoptr i32 %prev to ptr
|
||||
%pnp = getelementptr i8, ptr %pp, i32 4
|
||||
store i32 %nxt, ptr %pnp
|
||||
br label %split
|
||||
|
||||
split: ; give the tail back if it is worth it
|
||||
%floor = add i32 %n, 40
|
||||
%loose = icmp sge i32 %sz, %floor
|
||||
br i1 %loose, label %do.split, label %take.done
|
||||
do.split:
|
||||
store i32 %n, ptr %cp
|
||||
%tail = sub i32 %sz, %n
|
||||
%tailsz = sub i32 %tail, 8
|
||||
%tailat0 = add i32 %cur, 8
|
||||
%tailat = add i32 %tailat0, %n
|
||||
%tp = inttoptr i32 %tailat to ptr
|
||||
store i32 %tailsz, ptr %tp
|
||||
%tnp = getelementptr i8, ptr %tp, i32 4
|
||||
%fh = load i32, ptr @ludic_free
|
||||
store i32 %fh, ptr %tnp
|
||||
store i32 %tailat, ptr @ludic_free
|
||||
br label %take.done
|
||||
take.done:
|
||||
%reuse = getelementptr i8, ptr %cp, i32 8
|
||||
ret ptr %reuse
|
||||
|
||||
bump: ; nothing reusable: take fresh memory
|
||||
%b0 = load i32, ptr @ludic_brk
|
||||
%new = icmp eq i32 %b0, 0
|
||||
br i1 %new, label %brk.init, label %brk.have
|
||||
brk.init:
|
||||
%hb = ptrtoint ptr @__heap_base to i32
|
||||
%hb7 = add i32 %hb, 7
|
||||
%hba = and i32 %hb7, -8
|
||||
br label %brk.have
|
||||
brk.have:
|
||||
%brk = phi i32 [ %b0, %bump ], [ %hba, %brk.init ]
|
||||
%hdr = add i32 %brk, 8
|
||||
%need = add i32 %hdr, %n
|
||||
%pages = call i32 @llvm.wasm.memory.size.i32(i32 0)
|
||||
%have = shl i32 %pages, 16
|
||||
%short = icmp ugt i32 %need, %have
|
||||
br i1 %short, label %grow, label %carve
|
||||
grow:
|
||||
%deficit = sub i32 %need, %have
|
||||
%round = add i32 %deficit, 65535
|
||||
%want = lshr i32 %round, 16
|
||||
%got = call i32 @llvm.wasm.memory.grow.i32(i32 0, i32 %want)
|
||||
%oom = icmp eq i32 %got, -1
|
||||
br i1 %oom, label %fail, label %carve
|
||||
fail:
|
||||
ret ptr null
|
||||
carve:
|
||||
%hp = inttoptr i32 %brk to ptr
|
||||
store i32 %n, ptr %hp
|
||||
store i32 %need, ptr @ludic_brk
|
||||
%up = inttoptr i32 %hdr to ptr
|
||||
ret ptr %up
|
||||
}
|
||||
|
||||
define void @free(ptr %p) {
|
||||
entry:
|
||||
%nil = icmp eq ptr %p, null
|
||||
br i1 %nil, label %out, label %push
|
||||
push:
|
||||
%pi = ptrtoint ptr %p to i32
|
||||
%hi = sub i32 %pi, 8
|
||||
%hp = inttoptr i32 %hi to ptr
|
||||
%np = getelementptr i8, ptr %hp, i32 4
|
||||
%fh = load i32, ptr @ludic_free
|
||||
store i32 %fh, ptr %np
|
||||
store i32 %hi, ptr @ludic_free
|
||||
br label %out
|
||||
out:
|
||||
ret void
|
||||
}
|
||||
|
||||
; calloc and realloc are here because LLVM *invents* calls to them. It
|
||||
; recognises the allocator by name, so an -O2 pass rewrites the
|
||||
; mem_alloc()+mem_set(0) that core.ludic's rt_init does into a single calloc.
|
||||
; The platform layer therefore has to be a complete enough libc for whatever the
|
||||
; optimiser lowers to, not just for what ludicc emits.
|
||||
define ptr @calloc(i32 %count, i32 %size) {
|
||||
entry:
|
||||
%n = mul i32 %count, %size
|
||||
%p = call ptr @malloc(i32 %n)
|
||||
%nil = icmp eq ptr %p, null
|
||||
br i1 %nil, label %out, label %zero
|
||||
zero:
|
||||
call void @llvm.memset.p0.i32(ptr %p, i8 0, i32 %n, i1 false)
|
||||
br label %out
|
||||
out:
|
||||
ret ptr %p
|
||||
}
|
||||
|
||||
; the block header carries its own size, so realloc can grow in place
|
||||
define ptr @realloc(ptr %p, i32 %n) {
|
||||
entry:
|
||||
%nil = icmp eq ptr %p, null
|
||||
br i1 %nil, label %fresh, label %known
|
||||
fresh:
|
||||
%f = call ptr @malloc(i32 %n)
|
||||
ret ptr %f
|
||||
known:
|
||||
%pi = ptrtoint ptr %p to i32
|
||||
%hi = sub i32 %pi, 8
|
||||
%hp = inttoptr i32 %hi to ptr
|
||||
%old = load i32, ptr %hp
|
||||
%big = icmp sge i32 %old, %n
|
||||
br i1 %big, label %keep, label %move
|
||||
keep:
|
||||
ret ptr %p
|
||||
move:
|
||||
%new = call ptr @malloc(i32 %n)
|
||||
%bad = icmp eq ptr %new, null
|
||||
br i1 %bad, label %fail, label %shift
|
||||
fail:
|
||||
ret ptr null
|
||||
shift:
|
||||
call void @llvm.memcpy.p0.p0.i32(ptr %new, ptr %p, i32 %old, i1 false)
|
||||
call void @free(ptr %p)
|
||||
ret ptr %new
|
||||
}
|
||||
|
||||
; ---------------------------------------------------------------------------
|
||||
; Bulk memory and strings.
|
||||
;
|
||||
; size_t is 32-bit on wasm32, which is the signature ludicc emits for this
|
||||
; target and the one LLVM canonicalises these names to. They use the wasm
|
||||
; bulk-memory instructions (memory.copy / memory.fill) rather than a byte loop.
|
||||
; ---------------------------------------------------------------------------
|
||||
|
||||
define ptr @memcpy(ptr %d, ptr %s, i32 %n) {
|
||||
call void @llvm.memcpy.p0.p0.i32(ptr %d, ptr %s, i32 %n, i1 false)
|
||||
ret ptr %d
|
||||
}
|
||||
|
||||
define ptr @memset(ptr %d, i32 %c, i32 %n) {
|
||||
%b = trunc i32 %c to i8
|
||||
call void @llvm.memset.p0.i32(ptr %d, i8 %b, i32 %n, i1 false)
|
||||
ret ptr %d
|
||||
}
|
||||
|
||||
define ptr @memmove(ptr %d, ptr %s, i32 %n) {
|
||||
call void @llvm.memmove.p0.p0.i32(ptr %d, ptr %s, i32 %n, i1 false)
|
||||
ret ptr %d
|
||||
}
|
||||
|
||||
define i32 @memcmp(ptr %a, ptr %b, i32 %n) {
|
||||
entry:
|
||||
br label %loop
|
||||
loop:
|
||||
%i = phi i32 [ 0, %entry ], [ %i1, %step ]
|
||||
%done = icmp uge i32 %i, %n
|
||||
br i1 %done, label %same, label %test
|
||||
test:
|
||||
%ap = getelementptr i8, ptr %a, i32 %i
|
||||
%bp = getelementptr i8, ptr %b, i32 %i
|
||||
%av = load i8, ptr %ap
|
||||
%bv = load i8, ptr %bp
|
||||
%eq = icmp eq i8 %av, %bv
|
||||
br i1 %eq, label %step, label %differ
|
||||
step:
|
||||
%i1 = add i32 %i, 1
|
||||
br label %loop
|
||||
differ:
|
||||
%az = zext i8 %av to i32
|
||||
%bz = zext i8 %bv to i32
|
||||
%r = sub i32 %az, %bz
|
||||
ret i32 %r
|
||||
same:
|
||||
ret i32 0
|
||||
}
|
||||
|
||||
; LLVM emits bcmp for comparisons whose result is only tested against zero.
|
||||
define i32 @bcmp(ptr %a, ptr %b, i32 %n) {
|
||||
%r = call i32 @memcmp(ptr %a, ptr %b, i32 %n)
|
||||
ret i32 %r
|
||||
}
|
||||
|
||||
define i32 @strlen(ptr %s) {
|
||||
entry:
|
||||
br label %loop
|
||||
loop:
|
||||
%i = phi i32 [ 0, %entry ], [ %i1, %step ]
|
||||
%cp = getelementptr i8, ptr %s, i32 %i
|
||||
%c = load i8, ptr %cp
|
||||
%z = icmp eq i8 %c, 0
|
||||
br i1 %z, label %out, label %step
|
||||
step:
|
||||
%i1 = add i32 %i, 1
|
||||
br label %loop
|
||||
out:
|
||||
ret i32 %i
|
||||
}
|
||||
|
||||
; ---------------------------------------------------------------------------
|
||||
; Everything genuinely outside the module.
|
||||
;
|
||||
; These are wasm imports: platform.js supplies them, exactly as Cocoa supplies
|
||||
; objc_msgSend to cocoa.ll. Files are served from a preloaded read-only image of
|
||||
; the game's assets; the save file is backed by localStorage.
|
||||
; ---------------------------------------------------------------------------
|
||||
|
||||
declare i32 @web_fopen(ptr, ptr) ; -> handle, 0 on failure
|
||||
declare i32 @web_fread(i32, ptr, i32)
|
||||
declare i32 @web_fwrite(i32, ptr, i32)
|
||||
declare void @web_fclose(i32)
|
||||
declare i32 @web_fseek(i32, i32, i32)
|
||||
declare i32 @web_ftell(i32)
|
||||
declare i32 @web_time()
|
||||
declare void @web_exit(i32)
|
||||
|
||||
; A file handle is an opaque `ptr` to the Ludic runtime, and a small integer to
|
||||
; JS. Handle 0 doubles as libc's NULL, so `file_open` failure checks work
|
||||
; unchanged.
|
||||
define ptr @fopen(ptr %path, ptr %mode) {
|
||||
%h = call i32 @web_fopen(ptr %path, ptr %mode)
|
||||
%p = inttoptr i32 %h to ptr
|
||||
ret ptr %p
|
||||
}
|
||||
|
||||
define i32 @fread(ptr %buf, i32 %size, i32 %count, ptr %f) {
|
||||
%bytes = mul i32 %size, %count
|
||||
%h = ptrtoint ptr %f to i32
|
||||
%got = call i32 @web_fread(i32 %h, ptr %buf, i32 %bytes)
|
||||
ret i32 %got
|
||||
}
|
||||
|
||||
define i32 @fwrite(ptr %buf, i32 %size, i32 %count, ptr %f) {
|
||||
%bytes = mul i32 %size, %count
|
||||
%h = ptrtoint ptr %f to i32
|
||||
%put = call i32 @web_fwrite(i32 %h, ptr %buf, i32 %bytes)
|
||||
ret i32 %put
|
||||
}
|
||||
|
||||
define i32 @fclose(ptr %f) {
|
||||
%h = ptrtoint ptr %f to i32
|
||||
call void @web_fclose(i32 %h)
|
||||
ret i32 0
|
||||
}
|
||||
|
||||
define i32 @fseek(ptr %f, i32 %off, i32 %whence) {
|
||||
%h = ptrtoint ptr %f to i32
|
||||
%r = call i32 @web_fseek(i32 %h, i32 %off, i32 %whence)
|
||||
ret i32 %r
|
||||
}
|
||||
|
||||
define i32 @ftell(ptr %f) {
|
||||
%h = ptrtoint ptr %f to i32
|
||||
%r = call i32 @web_ftell(i32 %h)
|
||||
ret i32 %r
|
||||
}
|
||||
|
||||
define i64 @time(ptr %slot) {
|
||||
%s = call i32 @web_time()
|
||||
%r = sext i32 %s to i64
|
||||
ret i64 %r
|
||||
}
|
||||
|
||||
define void @exit(i32 %code) noreturn {
|
||||
call void @web_exit(i32 %code)
|
||||
unreachable
|
||||
}
|
||||
|
||||
; stdin/stdout. A windowed web build never reads stdin — rt_poll() goes through
|
||||
; win_poll() — but the intrinsic still has to resolve, and the headless path
|
||||
; (used by the test suite under Node) genuinely uses both.
|
||||
declare i32 @web_getchar()
|
||||
declare void @web_putchar(i32)
|
||||
|
||||
define i32 @getchar() {
|
||||
%c = call i32 @web_getchar()
|
||||
ret i32 %c
|
||||
}
|
||||
|
||||
define i32 @putchar(i32 %c) {
|
||||
call void @web_putchar(i32 %c)
|
||||
ret i32 %c
|
||||
}
|
||||
|
||||
; `print_str` is the one intrinsic ludicc lowers differently for the web: on a
|
||||
; native target it goes through variadic printf("%s"), which wasm's strict
|
||||
; signature checking makes awkward, so the web backend emits a direct call to
|
||||
; @web_print_str instead. It is a pure import — platform.js defines it — and so
|
||||
; needs no adapter here.
|
||||
Loading…
Add table
Add a link
Reference in a new issue