diff --git a/runtime/native/atlas.ludic b/runtime/native/atlas.ludic index ef550c09..24fa1cfb 100644 --- a/runtime/native/atlas.ludic +++ b/runtime/native/atlas.ludic @@ -27,21 +27,31 @@ const ATLAS_MAX_SHEET: int = 32 const ATLAS_MAX_SPR: int = 1024 const ATLAS_MAX_NAME: int = 512 -var at_sheet_img: words = null # image id per sheet -var at_sheet_cw: words = null # cell width per sheet -var at_sheet_ch: words = null # cell height per sheet -var at_nsheet: int = 0 +export state RtAtlasState { + at_sheet_img: words = null # image id per sheet + at_sheet_cw: words = null # cell width per sheet + at_sheet_ch: words = null # cell height per sheet + at_nsheet: int = 0 + at_spr_sheet: words = null # owning sheet per atlas sprite + at_spr_sx: words = null # source x/y (px) of the sub-rect + at_spr_sy: words = null + at_spr_w: words = null # sub-rect width/height (px) + at_spr_h: words = null + at_nspr: int = 0 + at_name_str: pointers = null # name per named sprite + at_name_id: words = null # atlas id per name + at_nname: int = 0 + at_q_name: pointers = null # name to register the loaded sprite under + at_q_path: pointers = null # file path to load + at_q_n: int = 0 # how many enqueued + at_q_pos: int = 0 # how many loaded so far + at_q_announced: bool = false + at_font_name: pointers = null + at_font_id: words = null + at_font_n: int = 0 +} -var at_spr_sheet: words = null # owning sheet per atlas sprite -var at_spr_sx: words = null # source x/y (px) of the sub-rect -var at_spr_sy: words = null -var at_spr_w: words = null # sub-rect width/height (px) -var at_spr_h: words = null -var at_nspr: int = 0 -var at_name_str: pointers = null # name per named sprite -var at_name_id: words = null # atlas id per name -var at_nname: int = 0 # #82 — an incremental preload queue: enqueue named image files, then load a # bounded number per frame (Assets.pump) so a loading scene stays responsive and @@ -50,136 +60,132 @@ var at_nname: int = 0 import "audio.ludic" # the preload queue feeds .wav/.mp3 into the sound bank const ATLAS_MAX_QUEUE: int = 512 -var at_q_name: pointers = null # name to register the loaded sprite under -var at_q_path: pointers = null # file path to load -var at_q_n: int = 0 # how many enqueued -var at_q_pos: int = 0 # how many loaded so far -function atlas_init() -> void { - if at_sheet_img != null { return } - at_sheet_img = words(ATLAS_MAX_SHEET) - at_sheet_cw = words(ATLAS_MAX_SHEET) - at_sheet_ch = words(ATLAS_MAX_SHEET) - at_spr_sheet = words(ATLAS_MAX_SPR) - at_spr_sx = words(ATLAS_MAX_SPR) - at_spr_sy = words(ATLAS_MAX_SPR) - at_spr_w = words(ATLAS_MAX_SPR) - at_spr_h = words(ATLAS_MAX_SPR) - at_name_str = pointers(ATLAS_MAX_NAME) # a pointer (8 bytes) per name slot - at_name_id = words(ATLAS_MAX_NAME) - at_q_name = pointers(ATLAS_MAX_QUEUE) - at_q_path = pointers(ATLAS_MAX_QUEUE) +function atlas_init(rt_atlas_st: mut RtAtlasState) -> void { + if rt_atlas_st.at_sheet_img != null { return } + rt_atlas_st.at_sheet_img = words(ATLAS_MAX_SHEET) + rt_atlas_st.at_sheet_cw = words(ATLAS_MAX_SHEET) + rt_atlas_st.at_sheet_ch = words(ATLAS_MAX_SHEET) + rt_atlas_st.at_spr_sheet = words(ATLAS_MAX_SPR) + rt_atlas_st.at_spr_sx = words(ATLAS_MAX_SPR) + rt_atlas_st.at_spr_sy = words(ATLAS_MAX_SPR) + rt_atlas_st.at_spr_w = words(ATLAS_MAX_SPR) + rt_atlas_st.at_spr_h = words(ATLAS_MAX_SPR) + rt_atlas_st.at_name_str = pointers(ATLAS_MAX_NAME) # a pointer (8 bytes) per name slot + rt_atlas_st.at_name_id = words(ATLAS_MAX_NAME) + rt_atlas_st.at_q_name = pointers(ATLAS_MAX_QUEUE) + rt_atlas_st.at_q_path = pointers(ATLAS_MAX_QUEUE) } # register (name -> atlas id) in the name table so Assets.get / Sprite.named find it. -function atlas_register_name(name: pointer, id: int) -> void { - atlas_init() - if at_nname >= ATLAS_MAX_NAME { return } - at_name_str[at_nname] = name - at_name_id[at_nname] = id - at_nname += 1 +function atlas_register_name(rt_atlas_st: mut RtAtlasState, name: pointer, id: int) -> void { + atlas_init(rt_atlas_st) + if rt_atlas_st.at_nname >= ATLAS_MAX_NAME { return } + rt_atlas_st.at_name_str[rt_atlas_st.at_nname] = name + rt_atlas_st.at_name_id[rt_atlas_st.at_nname] = id + rt_atlas_st.at_nname += 1 } # load a spritesheet PNG whose cells are cw x ch px; returns a sheet handle (>= 0). -function atlas_sheet(path: pointer, cw: int, ch: int) -> int { - atlas_init() - if at_nsheet >= ATLAS_MAX_SHEET { return -1 } - let img = rt_image_load(path) +function atlas_sheet(rt_atlas_st: mut RtAtlasState, rt_image_st: mut RtImageState, rt_inflate_st: mut RtInflateState, path: pointer, cw: int, ch: int) -> int { + atlas_init(rt_atlas_st) + if rt_atlas_st.at_nsheet >= ATLAS_MAX_SHEET { return -1 } + let img = rt_image_load(rt_image_st, rt_inflate_st, path) if img < 0 { return -1 } - let h = at_nsheet - at_sheet_img[h] = img - at_sheet_cw[h] = cw - at_sheet_ch[h] = ch - at_nsheet += 1 + let h = rt_atlas_st.at_nsheet + rt_atlas_st.at_sheet_img[h] = img + rt_atlas_st.at_sheet_cw[h] = cw + rt_atlas_st.at_sheet_ch[h] = ch + rt_atlas_st.at_nsheet += 1 return h } # register an atlas sprite for a sub-rect (px) of a sheet's image. -function atlas_make(sheet: int, sx: int, sy: int, w: int, h: int) -> int { - atlas_init() - if at_nspr >= ATLAS_MAX_SPR { return -1 } - let id = at_nspr - at_spr_sheet[id] = sheet - at_spr_sx[id] = sx - at_spr_sy[id] = sy - at_spr_w[id] = w - at_spr_h[id] = h - at_nspr += 1 +function atlas_make(rt_atlas_st: mut RtAtlasState, sheet: int, sx: int, sy: int, w: int, h: int) -> int { + atlas_init(rt_atlas_st) + if rt_atlas_st.at_nspr >= ATLAS_MAX_SPR { return -1 } + let id = rt_atlas_st.at_nspr + rt_atlas_st.at_spr_sheet[id] = sheet + rt_atlas_st.at_spr_sx[id] = sx + rt_atlas_st.at_spr_sy[id] = sy + rt_atlas_st.at_spr_w[id] = w + rt_atlas_st.at_spr_h[id] = h + rt_atlas_st.at_nspr += 1 return id } # one cell (col, row) of a sheet, addressed by grid coords. -function atlas_cell(sheet: int, col: int, row: int) -> int { - if (sheet < 0) or (sheet >= at_nsheet) { return -1 } - let cw = at_sheet_cw[sheet] - let ch = at_sheet_ch[sheet] - return atlas_make(sheet, col * cw, row * ch, cw, ch) +function atlas_cell(rt_atlas_st: mut RtAtlasState, sheet: int, col: int, row: int) -> int { + if (sheet < 0) or (sheet >= rt_atlas_st.at_nsheet) { return -1 } + let cw = rt_atlas_st.at_sheet_cw[sheet] + let ch = rt_atlas_st.at_sheet_ch[sheet] + return atlas_make(rt_atlas_st, sheet, col * cw, row * ch, cw, ch) } # a sprite spanning cols x rows cells from (col, row) — some sprites cover more # than one cell (a tall character, a wide object). -function atlas_cell_span(sheet: int, col: int, row: int, cols: int, rows: int) -> int { - if (sheet < 0) or (sheet >= at_nsheet) { return -1 } - let cw = at_sheet_cw[sheet] - let ch = at_sheet_ch[sheet] - return atlas_make(sheet, col * cw, row * ch, cols * cw, rows * ch) +function atlas_cell_span(rt_atlas_st: mut RtAtlasState, sheet: int, col: int, row: int, cols: int, rows: int) -> int { + if (sheet < 0) or (sheet >= rt_atlas_st.at_nsheet) { return -1 } + let cw = rt_atlas_st.at_sheet_cw[sheet] + let ch = rt_atlas_st.at_sheet_ch[sheet] + return atlas_make(rt_atlas_st, sheet, col * cw, row * ch, cols * cw, rows * ch) } # name a cell so a game can look it up by name; returns the atlas id. -function atlas_define(name: pointer, sheet: int, col: int, row: int) -> int { - atlas_init() - let id = atlas_cell(sheet, col, row) +function atlas_define(rt_atlas_st: mut RtAtlasState, name: pointer, sheet: int, col: int, row: int) -> int { + atlas_init(rt_atlas_st) + let id = atlas_cell(rt_atlas_st, sheet, col, row) if id < 0 { return -1 } - if at_nname < ATLAS_MAX_NAME { - at_name_str[at_nname] = name - at_name_id[at_nname] = id - at_nname += 1 + if rt_atlas_st.at_nname < ATLAS_MAX_NAME { + rt_atlas_st.at_name_str[rt_atlas_st.at_nname] = name + rt_atlas_st.at_name_id[rt_atlas_st.at_nname] = id + rt_atlas_st.at_nname += 1 } return id } # look up a named sprite's atlas id (names compare by content), or -1. -function atlas_named(name: pointer) -> int { - atlas_init() +function atlas_named(rt_atlas_st: mut RtAtlasState, name: pointer) -> int { + atlas_init(rt_atlas_st) var i = 0 - while i < at_nname { - if at_name_str[i] == name { return at_name_id[i] } + while i < rt_atlas_st.at_nname { + if rt_atlas_st.at_name_str[i] == name { return rt_atlas_st.at_name_id[i] } i += 1 } return -1 } # load a whole image file as one atlas sprite (a 1x1 sheet the size of the image). -function atlas_image(path: pointer) -> int { - atlas_init() - if at_nsheet >= ATLAS_MAX_SHEET { return -1 } - let img = rt_image_load(path) +function atlas_image(rt_atlas_st: mut RtAtlasState, rt_image_st: mut RtImageState, rt_inflate_st: mut RtInflateState, path: pointer) -> int { + atlas_init(rt_atlas_st) + if rt_atlas_st.at_nsheet >= ATLAS_MAX_SHEET { return -1 } + let img = rt_image_load(rt_image_st, rt_inflate_st, path) if img < 0 { return -1 } - let w = img_w[img] - let h = img_h[img] - let sh = at_nsheet - at_sheet_img[sh] = img - at_sheet_cw[sh] = w - at_sheet_ch[sh] = h - at_nsheet += 1 - return atlas_make(sh, 0, 0, w, h) + let w = rt_image_st.img_w[img] + let h = rt_image_st.img_h[img] + let sh = rt_atlas_st.at_nsheet + rt_atlas_st.at_sheet_img[sh] = img + rt_atlas_st.at_sheet_cw[sh] = w + rt_atlas_st.at_sheet_ch[sh] = h + rt_atlas_st.at_nsheet += 1 + return atlas_make(rt_atlas_st, sh, 0, 0, w, h) } # blit an atlas sprite at (dx, dy) through rt_put_px (camera / zoom / clip apply). # Only sufficiently-opaque pixels are drawn (the sprite's transparent border is # skipped), matching Screen.sprite. -function atlas_draw(id: int, dx: int, dy: int) -> void { - atlas_init() - if (id < 0) or (id >= at_nspr) { return } - let sheet = at_spr_sheet[id] - let img = at_sheet_img[sheet] - let iw = img_w[img] - let ih = img_h[img] - let s: words = img_px[img] - let sx = at_spr_sx[id] - let sy = at_spr_sy[id] - let w = at_spr_w[id] - let h = at_spr_h[id] +function atlas_draw(rt_atlas_st: mut RtAtlasState, rt_core_st: mut RtCoreState, rt_image_st: RtImageState, id: int, dx: int, dy: int) -> void { + atlas_init(rt_atlas_st) + if (id < 0) or (id >= rt_atlas_st.at_nspr) { return } + let sheet = rt_atlas_st.at_spr_sheet[id] + let img = rt_atlas_st.at_sheet_img[sheet] + let iw = rt_image_st.img_w[img] + let ih = rt_image_st.img_h[img] + let s: words = rt_image_st.img_px[img] + let sx = rt_atlas_st.at_spr_sx[id] + let sy = rt_atlas_st.at_spr_sy[id] + let w = rt_atlas_st.at_spr_w[id] + let h = rt_atlas_st.at_spr_h[id] var y = 0 while y < h { let srcy = sy + y @@ -189,7 +195,7 @@ function atlas_draw(id: int, dx: int, dy: int) -> void { let srcx = sx + x if (srcx >= 0) and (srcx < iw) { let px = s[srcy * iw + srcx] - if ((px >> 24) & 255) >= 128 { rt_put_px(dx + x, dy + y, px & 16777215) } + if ((px >> 24) & 255) >= 128 { rt_put_px(rt_core_st, dx + x, dy + y, px & 16777215) } } x += 1 } @@ -199,19 +205,19 @@ function atlas_draw(id: int, dx: int, dy: int) -> void { } # blit an atlas sprite scaled by an integer factor (through rt_fill_rect blocks). -function atlas_draw_scaled(id: int, dx: int, dy: int, sc: int) -> void { - atlas_init() - if (id < 0) or (id >= at_nspr) { return } +function atlas_draw_scaled(rt_atlas_st: mut RtAtlasState, rt_core_st: mut RtCoreState, rt_image_st: RtImageState, id: int, dx: int, dy: int, sc: int) -> void { + atlas_init(rt_atlas_st) + if (id < 0) or (id >= rt_atlas_st.at_nspr) { return } if sc < 1 { return } - let sheet = at_spr_sheet[id] - let img = at_sheet_img[sheet] - let iw = img_w[img] - let ih = img_h[img] - let s: words = img_px[img] - let sx = at_spr_sx[id] - let sy = at_spr_sy[id] - let w = at_spr_w[id] - let h = at_spr_h[id] + let sheet = rt_atlas_st.at_spr_sheet[id] + let img = rt_atlas_st.at_sheet_img[sheet] + let iw = rt_image_st.img_w[img] + let ih = rt_image_st.img_h[img] + let s: words = rt_image_st.img_px[img] + let sx = rt_atlas_st.at_spr_sx[id] + let sy = rt_atlas_st.at_spr_sy[id] + let w = rt_atlas_st.at_spr_w[id] + let h = rt_atlas_st.at_spr_h[id] var y = 0 while y < h { let srcy = sy + y @@ -221,7 +227,7 @@ function atlas_draw_scaled(id: int, dx: int, dy: int, sc: int) -> void { let srcx = sx + x if (srcx >= 0) and (srcx < iw) { let px = s[srcy * iw + srcx] - if ((px >> 24) & 255) >= 128 { rt_fill_rect(dx + x * sc, dy + y * sc, sc, sc, px & 16777215) } + if ((px >> 24) & 255) >= 128 { rt_fill_rect(rt_core_st, dx + x * sc, dy + y * sc, sc, sc, px & 16777215) } } x += 1 } @@ -233,20 +239,20 @@ function atlas_draw_scaled(id: int, dx: int, dy: int, sc: int) -> void { # blit an atlas sprite with the full Sprite-component treatment (#90): integer scale, # horizontal flip, and a solid tint (non-zero = every opaque pixel in that colour). # This is what the engine sprite-render system calls for `Sprite { atlas: 1 }`. -function atlas_draw_ex(id: int, dx: int, dy: int, sc0: int, flip: int, tint: int) -> void { - atlas_init() - if (id < 0) or (id >= at_nspr) { return } +function atlas_draw_ex(rt_atlas_st: mut RtAtlasState, rt_core_st: mut RtCoreState, rt_image_st: RtImageState, id: int, dx: int, dy: int, sc0: int, flip: int, tint: int) -> void { + atlas_init(rt_atlas_st) + if (id < 0) or (id >= rt_atlas_st.at_nspr) { return } var sc = sc0 if sc < 1 { sc = 1 } - let sheet = at_spr_sheet[id] - let img = at_sheet_img[sheet] - let iw = img_w[img] - let ih = img_h[img] - let s: words = img_px[img] - let sx = at_spr_sx[id] - let sy = at_spr_sy[id] - let w = at_spr_w[id] - let h = at_spr_h[id] + let sheet = rt_atlas_st.at_spr_sheet[id] + let img = rt_atlas_st.at_sheet_img[sheet] + let iw = rt_image_st.img_w[img] + let ih = rt_image_st.img_h[img] + let s: words = rt_image_st.img_px[img] + let sx = rt_atlas_st.at_spr_sx[id] + let sy = rt_atlas_st.at_spr_sy[id] + let w = rt_atlas_st.at_spr_w[id] + let h = rt_atlas_st.at_spr_h[id] var y = 0 while y < h { let srcy = sy + y @@ -261,8 +267,8 @@ function atlas_draw_ex(id: int, dx: int, dy: int, sc0: int, flip: int, tint: int if ((px >> 24) & 255) >= 128 { var col = px & 16777215 if tint != 0 { col = tint & 16777215 } - if sc == 1 { rt_put_px(dx + x, dy + y, col) } - else { rt_fill_rect(dx + x * sc, dy + y * sc, sc, sc, col) } + if sc == 1 { rt_put_px(rt_core_st, dx + x, dy + y, col) } + else { rt_fill_rect(rt_core_st, dx + x * sc, dy + y * sc, sc, sc, col) } } } x += 1 @@ -274,11 +280,11 @@ function atlas_draw_ex(id: int, dx: int, dy: int, sc0: int, flip: int, tint: int # a strip of `count` frames starting at (col,row), each `rows` cells tall, registered # as consecutive ids so `first + SpriteAnim.frame` addresses the current frame. -function atlas_strip(sheet: int, col: int, row: int, count: int, rows: int) -> int { +function atlas_strip(rt_atlas_st: mut RtAtlasState, sheet: int, col: int, row: int, count: int, rows: int) -> int { var first = -1 var i = 0 while i < count { - let id = atlas_cell_span(sheet, col + i, row, 1, rows) + let id = atlas_cell_span(rt_atlas_st, sheet, col + i, row, 1, rows) if first < 0 { first = id } i += 1 } @@ -286,26 +292,26 @@ function atlas_strip(sheet: int, col: int, row: int, count: int, rows: int) -> i } # an atlas sprite's pixel size — handy for centering / layout. -function atlas_width(id: int) -> int { atlas_init(); if (id < 0) or (id >= at_nspr) { return 0 }; return at_spr_w[id] } -function atlas_height(id: int) -> int { atlas_init(); if (id < 0) or (id >= at_nspr) { return 0 }; return at_spr_h[id] } +function atlas_width(rt_atlas_st: mut RtAtlasState, id: int) -> int { atlas_init(rt_atlas_st); if (id < 0) or (id >= rt_atlas_st.at_nspr) { return 0 }; return rt_atlas_st.at_spr_w[id] } +function atlas_height(rt_atlas_st: mut RtAtlasState, id: int) -> int { atlas_init(rt_atlas_st); if (id < 0) or (id >= rt_atlas_st.at_nspr) { return 0 }; return rt_atlas_st.at_spr_h[id] } # Sprite.draw_meter: `value` of `max` as a row of icons — full, half or empty per # `per_icon` points (hearts, stars, ammo pips), `spacing` px apart -function atlas_draw_meter(x: int, y: int, value: int, max: int, per_icon: int, spacing: int, full: int, half: int, empty: int) -> void { +function atlas_draw_meter(rt_atlas_st: mut RtAtlasState, rt_core_st: mut RtCoreState, rt_image_st: RtImageState, x: int, y: int, value: int, max: int, per_icon: int, spacing: int, full: int, half: int, empty: int) -> void { if per_icon <= 0 { return } var i = 0 while i < max / per_icon { var id = empty if value >= i * per_icon + per_icon / 2 { id = half } if value >= (i + 1) * per_icon { id = full } - atlas_draw(id, x + i * spacing, y) + atlas_draw(rt_atlas_st, rt_core_st, rt_image_st, id, x + i * spacing, y) i += 1 } } # Assets.enqueue_dir: every file of a directory, named by its file name without the # extension (assets/audio/hit.wav -> "hit"); the queue sorts by extension as usual -function assets_enqueue_dir(dir: pointer) -> void { +function assets_enqueue_dir(rt_atlas_st: mut RtAtlasState, dir: pointer) -> void { let names = Fs.list(dir) if names == null { return } var i = 0 @@ -313,44 +319,43 @@ function assets_enqueue_dir(dir: pointer) -> void { let file = names[i] var dot = len(file) - 1 while (dot > 0) and (file[dot] != '.') { dot -= 1 } - if dot > 0 { assets_enqueue(file[0..dot], dir + ("/") + file) } + if dot > 0 { assets_enqueue(rt_atlas_st, file[0..dot], dir + ("/") + file) } i += 1 } } # ---- incremental preload (#82) -------------------------------------------- # Enqueue a named image file to load later (does not load it now). -function assets_enqueue(name: pointer, path: pointer) -> void { - atlas_init() - if at_q_n >= ATLAS_MAX_QUEUE { return } - at_q_name[at_q_n] = name - at_q_path[at_q_n] = path - at_q_n += 1 +function assets_enqueue(rt_atlas_st: mut RtAtlasState, name: pointer, path: pointer) -> void { + atlas_init(rt_atlas_st) + if rt_atlas_st.at_q_n >= ATLAS_MAX_QUEUE { return } + rt_atlas_st.at_q_name[rt_atlas_st.at_q_n] = name + rt_atlas_st.at_q_path[rt_atlas_st.at_q_n] = path + rt_atlas_st.at_q_n += 1 } # Load up to `max` queued assets this frame, registering each under its name, and # return how many were loaded by this call. Call it each frame in a loading scene # (a small `max` keeps the frame short); Assets.ready() flips true when done. event AssetsReady { } # fired once, the frame the queue finishes -var at_q_announced: bool = false -function assets_pump(max: int) -> int { - atlas_init() +function assets_pump(rt_atlas_st: mut RtAtlasState, rt_audio_st: mut RtAudioState, rt_image_st: mut RtImageState, rt_inflate_st: mut RtInflateState, rt_truetype_st: mut RtTruetypeState, max: int) -> int { + atlas_init(rt_atlas_st) var done = 0 - while (done < max) and (at_q_pos < at_q_n) { - assets_load_one(at_q_name[at_q_pos], at_q_path[at_q_pos]) - at_q_pos += 1 + while (done < max) and (rt_atlas_st.at_q_pos < rt_atlas_st.at_q_n) { + assets_load_one(rt_atlas_st, rt_audio_st, rt_image_st, rt_inflate_st, rt_truetype_st, rt_atlas_st.at_q_name[rt_atlas_st.at_q_pos], rt_atlas_st.at_q_path[rt_atlas_st.at_q_pos]) + rt_atlas_st.at_q_pos += 1 done += 1 } - if (at_q_pos >= at_q_n) and (not at_q_announced) { at_q_announced = true; emit AssetsReady() } + if (rt_atlas_st.at_q_pos >= rt_atlas_st.at_q_n) and (not rt_atlas_st.at_q_announced) { rt_atlas_st.at_q_announced = true; emit AssetsReady() } return done } # the default loading bar of a `scene X loads then Y`: centred, a fifth of the screen wide -function assets_draw_progress() -> void { +function assets_draw_progress(rt_atlas_st: mut RtAtlasState, rt_core_st: mut RtCoreState) -> void { let w = rt_fbw / 2 let x = rt_fbw / 4 let y = rt_fbh / 2 - 4 - rt_fill_rect(x, y, w, 8, 0x303030) - rt_fill_rect(x, y, w * assets_progress() / 100, 8, 0xffcc44) + rt_fill_rect(rt_core_st, x, y, w, 8, 0x303030) + rt_fill_rect(rt_core_st, x, y, w * assets_progress(rt_atlas_st) / 100, 8, 0xffcc44) } # what kind of asset a path is, by its extension: sounds and fonts go to their own @@ -367,42 +372,39 @@ function path_has_suffix(path: pointer, suffix: pointer) -> bool { return true } const ATLAS_MAX_FONT: int = 16 -var at_font_name: pointers = null -var at_font_id: words = null -var at_font_n: int = 0 -function assets_load_one(name: pointer, path: pointer) -> void { +function assets_load_one(rt_atlas_st: mut RtAtlasState, rt_audio_st: mut RtAudioState, rt_image_st: mut RtImageState, rt_inflate_st: mut RtInflateState, rt_truetype_st: mut RtTruetypeState, name: pointer, path: pointer) -> void { if path_has_suffix(path, ".wav") or path_has_suffix(path, ".mp3") or path_has_suffix(path, ".ogg") { - audio_define(name, path) + audio_define(rt_audio_st, name, path) return } if path_has_suffix(path, ".ttf") or path_has_suffix(path, ".ttc") { - if at_font_name == null { at_font_name = pointers(ATLAS_MAX_FONT); at_font_id = words(ATLAS_MAX_FONT) } - if at_font_n < ATLAS_MAX_FONT { - at_font_name[at_font_n] = name - at_font_id[at_font_n] = rt_font_load(path) - at_font_n += 1 + if rt_atlas_st.at_font_name == null { rt_atlas_st.at_font_name = pointers(ATLAS_MAX_FONT); rt_atlas_st.at_font_id = words(ATLAS_MAX_FONT) } + if rt_atlas_st.at_font_n < ATLAS_MAX_FONT { + rt_atlas_st.at_font_name[rt_atlas_st.at_font_n] = name + rt_atlas_st.at_font_id[rt_atlas_st.at_font_n] = rt_font_load(rt_image_st, rt_truetype_st, path) + rt_atlas_st.at_font_n += 1 } return } - atlas_register_name(name, atlas_image(path)) + atlas_register_name(rt_atlas_st, name, atlas_image(rt_atlas_st, rt_image_st, rt_inflate_st, path)) } # the font handle registered under `name` (Assets.font), or 0 -function assets_font(name: pointer) -> int { +function assets_font(rt_atlas_st: RtAtlasState, name: pointer) -> int { var i = 0 - while i < at_font_n { - if at_font_name[i] == name { return at_font_id[i] } + while i < rt_atlas_st.at_font_n { + if rt_atlas_st.at_font_name[i] == name { return rt_atlas_st.at_font_id[i] } i += 1 } return 0 } -function assets_total() -> int { atlas_init(); return at_q_n } -function assets_loaded() -> int { atlas_init(); return at_q_pos } -function assets_ready() -> bool { atlas_init(); return at_q_pos >= at_q_n } +function assets_total(rt_atlas_st: mut RtAtlasState) -> int { atlas_init(rt_atlas_st); return rt_atlas_st.at_q_n } +function assets_loaded(rt_atlas_st: mut RtAtlasState) -> int { atlas_init(rt_atlas_st); return rt_atlas_st.at_q_pos } +function assets_ready(rt_atlas_st: mut RtAtlasState) -> bool { atlas_init(rt_atlas_st); return rt_atlas_st.at_q_pos >= rt_atlas_st.at_q_n } # loading progress as a whole-number percent (0..100); an empty queue is 100. -function assets_progress() -> int { - atlas_init() - if at_q_n <= 0 { return 100 } - return at_q_pos * 100 / at_q_n +function assets_progress(rt_atlas_st: mut RtAtlasState) -> int { + atlas_init(rt_atlas_st) + if rt_atlas_st.at_q_n <= 0 { return 100 } + return rt_atlas_st.at_q_pos * 100 / rt_atlas_st.at_q_n } diff --git a/runtime/native/audio.ludic b/runtime/native/audio.ludic index c34e5a2b..d894969c 100644 --- a/runtime/native/audio.ludic +++ b/runtime/native/audio.ludic @@ -19,67 +19,69 @@ const AUDIO_CAP: int = 64 # max simultaneously-loaded sounds -var snd_ready: bool = false -var snd_tab: pointers = null # handle (1-based) -> AVAudioPlayer pointer -var snd_master: fixed = 1.0 # master volume, applied to every play (and to audio_play_at's gain) -var snd_rate: fixed = 1.0 # playback rate / pitch (1.0 = normal) -var snd_music: int = 0 # the handle currently playing as music (0 = none) +export state RtAudioState { + snd_ready: bool = false + snd_tab: pointers = null # handle (1-based) -> AVAudioPlayer pointer + snd_master: fixed = 1.0 # master volume, applied to every play (and to audio_play_at's gain) + snd_rate: fixed = 1.0 # playback rate / pitch (1.0 = normal) + snd_music: int = 0 # the handle currently playing as music (0 = none) + snd_bank_name: pointers = null + snd_bank_id: words = null + snd_bank_n: int = 0 +} # the sound bank: sounds registered by name (Audio.define), played by name (Audio.play(name:)) -var snd_bank_name: pointers = null -var snd_bank_id: words = null -var snd_bank_n: int = 0 -function audio_init() -> void { - if snd_ready { return } - snd_tab = pointers(AUDIO_CAP) # one 8-byte pointer slot per handle - fill(snd_tab, 0, AUDIO_CAP * 8) # malloc does not zero; empty slots must read null - snd_bank_name = pointers(AUDIO_CAP) - snd_bank_id = words(AUDIO_CAP) - snd_ready = true +function audio_init(rt_audio_st: mut RtAudioState) -> void { + if rt_audio_st.snd_ready { return } + rt_audio_st.snd_tab = pointers(AUDIO_CAP) # one 8-byte pointer slot per handle + fill(rt_audio_st.snd_tab, 0, AUDIO_CAP * 8) # malloc does not zero; empty slots must read null + rt_audio_st.snd_bank_name = pointers(AUDIO_CAP) + rt_audio_st.snd_bank_id = words(AUDIO_CAP) + rt_audio_st.snd_ready = true } # ---- the sound bank: Audio.define(name, path) then Audio.play(name: "…") ------ # Load a sound and remember it under a name; returns the handle (0 headless). -function audio_define(name: pointer, path: pointer) -> int { - audio_init() - let id = audio_load(path) - if snd_bank_n < AUDIO_CAP { - snd_bank_name[snd_bank_n] = name - snd_bank_id[snd_bank_n] = id - snd_bank_n += 1 +function audio_define(rt_audio_st: mut RtAudioState, name: pointer, path: pointer) -> int { + audio_init(rt_audio_st) + let id = audio_load(rt_audio_st, path) + if rt_audio_st.snd_bank_n < AUDIO_CAP { + rt_audio_st.snd_bank_name[rt_audio_st.snd_bank_n] = name + rt_audio_st.snd_bank_id[rt_audio_st.snd_bank_n] = id + rt_audio_st.snd_bank_n += 1 } return id } # the handle registered under `name`, or 0 -function audio_named(name: pointer) -> int { - audio_init() +function audio_named(rt_audio_st: mut RtAudioState, name: pointer) -> int { + audio_init(rt_audio_st) var i = 0 - while i < snd_bank_n { - if snd_bank_name[i] == name { return snd_bank_id[i] } + while i < rt_audio_st.snd_bank_n { + if rt_audio_st.snd_bank_name[i] == name { return rt_audio_st.snd_bank_id[i] } i += 1 } return 0 } -function audio_play_named(name: pointer) -> void { audio_play(audio_named(name)) } -function audio_play_music_named(name: pointer) -> void { audio_play_music(audio_named(name)) } +function audio_play_named(rt_audio_st: mut RtAudioState, name: pointer) -> void { audio_play(rt_audio_st, audio_named(rt_audio_st, name)) } +function audio_play_music_named(rt_audio_st: mut RtAudioState, name: pointer) -> void { audio_play_music(rt_audio_st, audio_named(rt_audio_st, name)) } # Resolve a 1-based handle to its player pointer (null if out of range / empty). -function audio_get(id: int) -> pointer { - audio_init() +function audio_get(rt_audio_st: mut RtAudioState, id: int) -> pointer { + audio_init(rt_audio_st) if (id < 1) or (id > AUDIO_CAP) { return null } - return snd_tab[id - 1] + return rt_audio_st.snd_tab[id - 1] } # Load a sound file and return its handle (>= 1), or 0 on failure / headless. -function audio_load(path: pointer) -> int { - audio_init() +function audio_load(rt_audio_st: mut RtAudioState, path: pointer) -> int { + audio_init(rt_audio_st) if not is_windowed() { return 0 } var i = 0 while i < AUDIO_CAP { - if snd_tab[i] == null { + if rt_audio_st.snd_tab[i] == null { let p = snd_load(path) if p == null { return 0 } - snd_tab[i] = p + rt_audio_st.snd_tab[i] = p return i + 1 } i += 1 @@ -88,12 +90,12 @@ function audio_load(path: pointer) -> int { } # Fire a one-shot sound from the start. -function audio_play(id: int) -> void { +function audio_play(rt_audio_st: mut RtAudioState, id: int) -> void { if not is_windowed() { return } - let p = audio_get(id) + let p = audio_get(rt_audio_st, id) if p == null { return } snd_set_pan(p, 0.0) - snd_play(p, 0, snd_rate, snd_master) + snd_play(p, 0, rt_audio_st.snd_rate, rt_audio_st.snd_master) } # Fire a one-shot with its OWN gain, pitch and stereo position, leaving the master @@ -111,9 +113,9 @@ function audio_play(id: int) -> void { # The one honest limitation: a sound is one player, so firing the same handle again # restarts it rather than layering a second copy. Load a handle per variant when several # need to overlap - which is what a game does anyway to stop a repeated sound machine-gunning. -function audio_play_at(id: int, gain: fixed, pitch: fixed, pan: fixed) -> void { +function audio_play_at(rt_audio_st: mut RtAudioState, id: int, gain: fixed, pitch: fixed, pan: fixed) -> void { if not is_windowed() { return } - let p = audio_get(id) + let p = audio_get(rt_audio_st, id) if p == null { return } var g = gain if g < 0.0 { g = 0.0 } @@ -125,80 +127,80 @@ function audio_play_at(id: int, gain: fixed, pitch: fixed, pan: fixed) -> void { if pt < 0.25 { pt = 0.25 } # AVAudioPlayer's own rate range if pt > 4.0 { pt = 4.0 } snd_set_pan(p, pn) - snd_play(p, 0, pt, g * snd_master) + snd_play(p, 0, pt, g * rt_audio_st.snd_master) } # Play a sound as looping background music on the single music channel; any # previous music is stopped first. -function audio_play_music(id: int) -> void { +function audio_play_music(rt_audio_st: mut RtAudioState, id: int) -> void { if not is_windowed() { return } - let p = audio_get(id) + let p = audio_get(rt_audio_st, id) if p == null { return } - audio_stop_music() - snd_music = id - snd_play(p, -1, snd_rate, snd_master) + audio_stop_music(rt_audio_st) + rt_audio_st.snd_music = id + snd_play(p, -1, rt_audio_st.snd_rate, rt_audio_st.snd_master) } # Stop one sound. -function audio_stop(id: int) -> void { +function audio_stop(rt_audio_st: mut RtAudioState, id: int) -> void { if not is_windowed() { return } - let p = audio_get(id) + let p = audio_get(rt_audio_st, id) if p == null { return } snd_stop(p) - if id == snd_music { snd_music = 0 } + if id == rt_audio_st.snd_music { rt_audio_st.snd_music = 0 } } # Stop the current music channel. -function audio_stop_music() -> void { +function audio_stop_music(rt_audio_st: mut RtAudioState) -> void { if not is_windowed() { return } - if snd_music != 0 { - let p = audio_get(snd_music) + if rt_audio_st.snd_music != 0 { + let p = audio_get(rt_audio_st, rt_audio_st.snd_music) if p != null { snd_stop(p) } - snd_music = 0 + rt_audio_st.snd_music = 0 } } # Stop every loaded sound. -function audio_stop_all() -> void { +function audio_stop_all(rt_audio_st: mut RtAudioState) -> void { if not is_windowed() { return } - audio_init() + audio_init(rt_audio_st) var i = 0 while i < AUDIO_CAP { - if snd_tab[i] != null { snd_stop(snd_tab[i]) } + if rt_audio_st.snd_tab[i] != null { snd_stop(rt_audio_st.snd_tab[i]) } i += 1 } - snd_music = 0 + rt_audio_st.snd_music = 0 } # Master volume (0.0 .. 1.0) — applied to every currently-loaded sound now and # to every future play. -function audio_volume(v: fixed) -> void { - snd_master = v +function audio_volume(rt_audio_st: mut RtAudioState, v: fixed) -> void { + rt_audio_st.snd_master = v if not is_windowed() { return } - audio_init() + audio_init(rt_audio_st) var i = 0 while i < AUDIO_CAP { - if snd_tab[i] != null { snd_set_volume(snd_tab[i], v) } + if rt_audio_st.snd_tab[i] != null { snd_set_volume(rt_audio_st.snd_tab[i], v) } i += 1 } } # Playback rate / pitch (1.0 = normal, 0.5 = an octave down, 2.0 = up). -function audio_pitch(v: fixed) -> void { - snd_rate = v +function audio_pitch(rt_audio_st: mut RtAudioState, v: fixed) -> void { + rt_audio_st.snd_rate = v if not is_windowed() { return } - audio_init() + audio_init(rt_audio_st) var i = 0 while i < AUDIO_CAP { - if snd_tab[i] != null { snd_set_rate(snd_tab[i], v) } + if rt_audio_st.snd_tab[i] != null { snd_set_rate(rt_audio_st.snd_tab[i], v) } i += 1 } } # Is this sound currently playing? -function audio_is_playing(id: int) -> bool { +function audio_is_playing(rt_audio_st: mut RtAudioState, id: int) -> bool { if not is_windowed() { return false } - let p = audio_get(id) + let p = audio_get(rt_audio_st, id) if p == null { return false } return snd_playing(p) != 0 } diff --git a/runtime/native/core.ludic b/runtime/native/core.ludic index ed7cf041..48044054 100644 --- a/runtime/native/core.ludic +++ b/runtime/native/core.ludic @@ -23,11 +23,33 @@ # where a headless build leaves its last frame (relative to the working directory) const HEADLESS_FRAME_PATH: string = "build/out.ppm" -var rt_fb: words = null # framebuffer, one i32 (0x00RRGGBB) per pixel -var rt_fbw: int = 320 -var rt_fbh: int = 240 -var rt_regs: words = null # the 64 general-purpose game registers -var rt_alive: int = 1 # platform still running? +export state RtCoreState { + rt_fb: words = null # framebuffer, one i32 (0x00RRGGBB) per pixel + rt_regs: words = null # the 64 general-purpose game registers + rt_alive: int = 1 # platform still running? + rt_cam_x: int = 0 # camera base offset (world -> screen: subtracted) + rt_cam_y: int = 0 + rt_shake_x: int = 0 # transient screen-shake offset, added to the base + rt_shake_y: int = 0 + rt_clip_x0: int = 0 # clip rectangle in screen space (half-open) + rt_clip_y0: int = 0 + rt_clip_x1: int = 320 + rt_clip_y1: int = 240 + rt_blend: int = 0 # 0 = replace, 1 = additive + rt_cam_zoom: fixed = 1.0 # 1.0 = no zoom; >1 zooms in, <1 zooms out + rt_cam_zoomed: bool = false # true once a non-1.0 zoom is set + rt_shake_amount: int = 0 + rt_shake_left: int = 0 + rt_map: pointer = null + rt_mapw: int = 0 + rt_maph: int = 0 + rt_map_solid1: int = 0 + rt_map_solid2: int = 0 + rt_map_tile_px: int = 16 + rt_statusbuf: pointer = null +} +let rt_fbw: int = 320 +let rt_fbh: int = 240 # ---- renderer state (camera / clip / blend) ------------------------------- # A world-space camera offset, a clip rectangle, and a blend mode threaded @@ -36,23 +58,12 @@ var rt_alive: int = 1 # platform still running? # The defaults are neutral — camera (0,0), clip = full screen, blend = replace — # so a game that never touches them renders exactly as before. The camera moves # everything drawn; reset it to (0,0) to draw a fixed HUD over the world. -var rt_cam_x: int = 0 # camera base offset (world -> screen: subtracted) -var rt_cam_y: int = 0 -var rt_shake_x: int = 0 # transient screen-shake offset, added to the base -var rt_shake_y: int = 0 -var rt_clip_x0: int = 0 # clip rectangle in screen space (half-open) -var rt_clip_y0: int = 0 -var rt_clip_x1: int = 320 -var rt_clip_y1: int = 240 -var rt_blend: int = 0 # 0 = replace, 1 = additive # #78 — deterministic camera zoom. A Q16.16 scale applied about the screen centre # in the same two chokepoints as the camera offset. Rejected floats for the # coordinate types (they would desync lockstep/replay/save); zoom is a *render-time* # transform, so it rides on fixed-point exactly like sprite-scale and the light math. # rt_cam_zoomed gates the fixed multiply out of the hot path so a game that never # zooms renders byte-for-byte identically (the else-branch is the original code). -var rt_cam_zoom: fixed = 1.0 # 1.0 = no zoom; >1 zooms in, <1 zooms out -var rt_cam_zoomed: bool = false # true once a non-1.0 zoom is set # 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. @@ -60,42 +71,42 @@ function rt_font() -> string { 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>7222BBAAAAA>NAAN@@@>AAAEB=NAANDBA>A@>1A>O444444AAAAAA>AAAAA:4AAAEEKAAA:4:AAAA:4444O1248@O" } -function rt_init() -> void { - rt_fb = words(320 * 240) - rt_regs = words(64) - fill(rt_regs, 0, 64 * 4) - rt_map = bytes(96 * 64) - fill(rt_map, ' ', 96 * 64) - rt_clip_x1 = rt_fbw - rt_clip_y1 = rt_fbh - rt_statusbuf = bytes(96) - rt_statusbuf[0] = 0 - rt_image_init() - rt_tt_init() - rt_ui_init() - rt_clear(0) +function rt_init(rt_core_st: mut RtCoreState, rt_image_st: mut RtImageState, rt_rng_st: mut RtRngState, rt_truetype_st: mut RtTruetypeState, rt_ui_st: mut RtUiState) -> void { + rt_core_st.rt_fb = words(320 * 240) + rt_core_st.rt_regs = words(64) + fill(rt_core_st.rt_regs, 0, 64 * 4) + rt_core_st.rt_map = bytes(96 * 64) + fill(rt_core_st.rt_map, ' ', 96 * 64) + rt_core_st.rt_clip_x1 = rt_fbw + rt_core_st.rt_clip_y1 = rt_fbh + rt_core_st.rt_statusbuf = bytes(96) + rt_core_st.rt_statusbuf[0] = 0 + rt_image_init(rt_image_st) + rt_tt_init(rt_truetype_st) + rt_ui_init(rt_ui_st) + rt_clear(rt_core_st, rt_rng_st, 0) if is_windowed() { win_open(rt_fbw, rt_fbh, 3, game_title()) } } -function rt_shutdown() -> void { +function rt_shutdown(rt_core_st: RtCoreState) -> void { if is_windowed() { win_close() return } - rt_dump_ppm(HEADLESS_FRAME_PATH) + rt_dump_ppm(rt_core_st, HEADLESS_FRAME_PATH) } # ---- framebuffer ---------------------------------------------------------- function rt_screen_w() -> int { return rt_fbw } function rt_screen_h() -> int { return rt_fbh } -function rt_clear(c: int) -> void { - rt_camera_tick() # a timed Camera.shake_for advances once per frame +function rt_clear(rt_core_st: mut RtCoreState, rt_rng_st: mut RtRngState, c: int) -> void { + rt_camera_tick(rt_core_st, rt_rng_st) # a timed Camera.shake_for advances once per frame let n = rt_fbw * rt_fbh for i in 0 .. n { - rt_fb[i] = c + rt_core_st.rt_fb[i] = c } } @@ -109,74 +120,74 @@ function rt_blend_add(dst: int, src: int) -> int { # the low-level plot: apply the camera (+ shake) offset, reject anything outside # the clip rectangle or the framebuffer, then write or additively blend. -function rt_put_px(x: int, y: int, c: int) -> void { - var sx = x - rt_cam_x - rt_shake_x - var sy = y - rt_cam_y - rt_shake_y - if rt_cam_zoomed { # #78: scale about the screen centre +function rt_put_px(rt_core_st: mut RtCoreState, x: int, y: int, c: int) -> void { + var sx = x - rt_core_st.rt_cam_x - rt_core_st.rt_shake_x + var sy = y - rt_core_st.rt_cam_y - rt_core_st.rt_shake_y + if rt_core_st.rt_cam_zoomed { # #78: scale about the screen centre let hw = rt_fbw / 2 let hh = rt_fbh / 2 - sx = floor(fixed(sx - hw) * rt_cam_zoom) + hw - sy = floor(fixed(sy - hh) * rt_cam_zoom) + hh + sx = floor(fixed(sx - hw) * rt_core_st.rt_cam_zoom) + hw + sy = floor(fixed(sy - hh) * rt_core_st.rt_cam_zoom) + hh } - if sx < rt_clip_x0 { return } - if sy < rt_clip_y0 { return } - if sx >= rt_clip_x1 { return } - if sy >= rt_clip_y1 { return } + if sx < rt_core_st.rt_clip_x0 { return } + if sy < rt_core_st.rt_clip_y0 { return } + if sx >= rt_core_st.rt_clip_x1 { return } + if sy >= rt_core_st.rt_clip_y1 { return } if sx < 0 { return } if sy < 0 { return } if sx >= rt_fbw { return } if sy >= rt_fbh { return } let idx = sy * rt_fbw + sx - if rt_blend == 1 { rt_fb[idx] = rt_blend_add(rt_fb[idx], c) } - else { rt_fb[idx] = c } + if rt_core_st.rt_blend == 1 { rt_core_st.rt_fb[idx] = rt_blend_add(rt_core_st.rt_fb[idx], c) } + else { rt_core_st.rt_fb[idx] = c } } -function rt_fill_rect(x: int, y: int, w: int, h: int, c: int) -> void { - var ox = x - rt_cam_x - rt_shake_x - var oy = y - rt_cam_y - rt_shake_y +function rt_fill_rect(rt_core_st: mut RtCoreState, x: int, y: int, w: int, h: int, c: int) -> void { + var ox = x - rt_core_st.rt_cam_x - rt_core_st.rt_shake_x + var oy = y - rt_core_st.rt_cam_y - rt_core_st.rt_shake_y var ow = w var oh = h - if rt_cam_zoomed { # #78: scale position + size about the centre + if rt_core_st.rt_cam_zoomed { # #78: scale position + size about the centre let hw = rt_fbw / 2 let hh = rt_fbh / 2 - ox = floor(fixed(ox - hw) * rt_cam_zoom) + hw - oy = floor(fixed(oy - hh) * rt_cam_zoom) + hh - ow = floor(fixed(w) * rt_cam_zoom); if ow < 1 { ow = 1 } - oh = floor(fixed(h) * rt_cam_zoom); if oh < 1 { oh = 1 } + ox = floor(fixed(ox - hw) * rt_core_st.rt_cam_zoom) + hw + oy = floor(fixed(oy - hh) * rt_core_st.rt_cam_zoom) + hh + ow = floor(fixed(w) * rt_core_st.rt_cam_zoom); if ow < 1 { ow = 1 } + oh = floor(fixed(h) * rt_core_st.rt_cam_zoom); if oh < 1 { oh = 1 } } - let x0 = max(max(0, rt_clip_x0), ox) - let y0 = max(max(0, rt_clip_y0), oy) - let x1 = min(min(rt_fbw, rt_clip_x1), ox + ow) - let y1 = min(min(rt_fbh, rt_clip_y1), oy + oh) + let x0 = max(max(0, rt_core_st.rt_clip_x0), ox) + let y0 = max(max(0, rt_core_st.rt_clip_y0), oy) + let x1 = min(min(rt_fbw, rt_core_st.rt_clip_x1), ox + ow) + let y1 = min(min(rt_fbh, rt_core_st.rt_clip_y1), oy + oh) var j = y0 while j < y1 { let row = j * rt_fbw var i = x0 while i < x1 { - if rt_blend == 1 { rt_fb[row + i] = rt_blend_add(rt_fb[row + i], c) } - else { rt_fb[row + i] = c } + if rt_core_st.rt_blend == 1 { rt_core_st.rt_fb[row + i] = rt_blend_add(rt_core_st.rt_fb[row + i], c) } + else { rt_core_st.rt_fb[row + i] = c } i += 1 } j += 1 } } -function 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) +function rt_frame_rect(rt_core_st: mut RtCoreState, x: int, y: int, w: int, h: int, c: int) -> void { + rt_fill_rect(rt_core_st, x, y, w, 1, c) + rt_fill_rect(rt_core_st, x, y + h - 1, w, 1, c) + rt_fill_rect(rt_core_st, x, y, 1, h, c) + rt_fill_rect(rt_core_st, x + w - 1, y, 1, h, c) } # A straight line by Bresenham's algorithm — integer only, any direction. -function rt_line(x0: int, y0: int, x1: int, y1: int, c: int) -> void { +function rt_line(rt_core_st: mut RtCoreState, x0: int, y0: int, x1: int, y1: int, c: int) -> void { var x = x0; var y = y0 let dx = abs(x1 - x0); let dy = -abs(y1 - y0) var sx = -1; if x0 < x1 { sx = 1 } var sy = -1; if y0 < y1 { sy = 1 } var err = dx + dy while true { - rt_put_px(x, y, c) + rt_put_px(rt_core_st, x, y, c) if (x == x1) and (y == y1) { return } let e2 = 2 * err if e2 >= dy { err += dy; x += sx } @@ -185,14 +196,14 @@ function rt_line(x0: int, y0: int, x1: int, y1: int, c: int) -> void { } # A circle outline by the midpoint algorithm (eight-way symmetry). -function rt_circle(cx: int, cy: int, r: int, c: int) -> void { +function rt_circle(rt_core_st: mut RtCoreState, cx: int, cy: int, r: int, c: int) -> void { if r < 0 { return } var x = r; var y = 0; var err = 1 - r while x >= y { - rt_put_px(cx + x, cy + y, c); rt_put_px(cx + y, cy + x, c) - rt_put_px(cx - y, cy + x, c); rt_put_px(cx - x, cy + y, c) - rt_put_px(cx - x, cy - y, c); rt_put_px(cx - y, cy - x, c) - rt_put_px(cx + y, cy - x, c); rt_put_px(cx + x, cy - y, c) + rt_put_px(rt_core_st, cx + x, cy + y, c); rt_put_px(rt_core_st, cx + y, cy + x, c) + rt_put_px(rt_core_st, cx - y, cy + x, c); rt_put_px(rt_core_st, cx - x, cy + y, c) + rt_put_px(rt_core_st, cx - x, cy - y, c); rt_put_px(rt_core_st, cx - y, cy - x, c) + rt_put_px(rt_core_st, cx + y, cy - x, c); rt_put_px(rt_core_st, cx + x, cy - y, c) y += 1 if err < 0 { err = err + 2 * y + 1 } else { x -= 1; err = err + 2 * (y - x) + 1 } @@ -200,27 +211,27 @@ function rt_circle(cx: int, cy: int, r: int, c: int) -> void { } # A filled disc — one horizontal span per row, width from the circle equation. -function rt_fill_circle(cx: int, cy: int, r: int, c: int) -> void { +function rt_fill_circle(rt_core_st: mut RtCoreState, cx: int, cy: int, r: int, c: int) -> void { if r < 0 { return } let r2 = r * r var dy = -r while dy <= r { var dx = 0 while (dx + 1) * (dx + 1) + dy * dy <= r2 { dx += 1 } - rt_fill_rect(cx - dx, cy + dy, 2 * dx + 1, 1, c) + rt_fill_rect(rt_core_st, cx - dx, cy + dy, 2 * dx + 1, 1, c) dy += 1 } } # A triangle outline — three lines. -function rt_triangle(x0: int, y0: int, x1: int, y1: int, x2: int, y2: int, c: int) -> void { - rt_line(x0, y0, x1, y1, c) - rt_line(x1, y1, x2, y2, c) - rt_line(x2, y2, x0, y0, c) +function rt_triangle(rt_core_st: mut RtCoreState, x0: int, y0: int, x1: int, y1: int, x2: int, y2: int, c: int) -> void { + rt_line(rt_core_st, x0, y0, x1, y1, c) + rt_line(rt_core_st, x1, y1, x2, y2, c) + rt_line(rt_core_st, x2, y2, x0, y0, c) } # A filled triangle — bounding-box scan with an edge-sign inside test. -function rt_fill_triangle(x0: int, y0: int, x1: int, y1: int, x2: int, y2: int, c: int) -> void { +function rt_fill_triangle(rt_core_st: mut RtCoreState, x0: int, y0: int, x1: int, y1: int, x2: int, y2: int, c: int) -> void { let minx = min(x0, min(x1, x2)); let maxx = max(x0, max(x1, x2)) let miny = min(y0, min(y1, y2)); let maxy = max(y0, max(y1, y2)) var py = miny @@ -232,7 +243,7 @@ function rt_fill_triangle(x0: int, y0: int, x1: int, y1: int, x2: int, y2: int, let d2 = (x0 - x2) * (py - y2) - (y0 - y2) * (px - x2) let neg = (d0 < 0) or (d1 < 0) or (d2 < 0) let pos = (d0 > 0) or (d1 > 0) or (d2 > 0) - if not (neg and pos) { rt_put_px(px, py, c) } + if not (neg and pos) { rt_put_px(rt_core_st, px, py, c) } px += 1 } py += 1 @@ -240,16 +251,16 @@ function rt_fill_triangle(x0: int, y0: int, x1: int, y1: int, x2: int, y2: int, } # the four-way symmetric points of an ellipse centred at (cx, cy). -function rt_oval_pts(cx: int, cy: int, x: int, y: int, c: int) -> void { - rt_put_px(cx + x, cy + y, c) - rt_put_px(cx - x, cy + y, c) - rt_put_px(cx + x, cy - y, c) - rt_put_px(cx - x, cy - y, c) +function rt_oval_pts(rt_core_st: mut RtCoreState, cx: int, cy: int, x: int, y: int, c: int) -> void { + rt_put_px(rt_core_st, cx + x, cy + y, c) + rt_put_px(rt_core_st, cx - x, cy + y, c) + rt_put_px(rt_core_st, cx + x, cy - y, c) + rt_put_px(rt_core_st, cx - x, cy - y, c) } # An axis-aligned ellipse outline by the midpoint algorithm — integer only, # radii rx (horizontal) and ry (vertical). rx == ry draws a circle. -function rt_oval(x: int, y: int, rx: int, ry: int, c: int) -> void { +function rt_oval(rt_core_st: mut RtCoreState, x: int, y: int, rx: int, ry: int, c: int) -> void { if rx <= 0 { return } if ry <= 0 { return } let rx2 = rx * rx @@ -260,14 +271,14 @@ function rt_oval(x: int, y: int, rx: int, ry: int, c: int) -> void { var ey = ry var px = 0 var py = two_rx2 * ey - rt_oval_pts(x, y, ex, ey, c) + rt_oval_pts(rt_core_st, x, y, ex, ey, c) var p = ry2 - rx2 * ry + rx2 / 4 # region 1 while px < py { ex += 1 px += two_ry2 if p < 0 { p = p + ry2 + px } else { ey -= 1; py -= two_rx2; p = p + ry2 + px - py } - rt_oval_pts(x, y, ex, ey, c) + rt_oval_pts(rt_core_st, x, y, ex, ey, c) } p = ry2 * (ex * 2 + 1) * (ex * 2 + 1) / 4 + rx2 * (ey - 1) * (ey - 1) - rx2 * ry2 # region 2 while ey > 0 { @@ -275,18 +286,18 @@ function rt_oval(x: int, y: int, rx: int, ry: int, c: int) -> void { py -= two_rx2 if p > 0 { p = p + rx2 - py } else { ex += 1; px += two_ry2; p = p + rx2 - py + px } - rt_oval_pts(x, y, ex, ey, c) + rt_oval_pts(rt_core_st, x, y, ex, ey, c) } } # Read a framebuffer pixel in screen space (0x00RRGGBB), or 0 if out of bounds. # Unlike the plot path this ignores the camera — it reads the actual screen. -function rt_get_px(x: int, y: int) -> int { +function rt_get_px(rt_core_st: RtCoreState, x: int, y: int) -> int { if x < 0 { return 0 } if y < 0 { return 0 } if x >= rt_fbw { return 0 } if y >= rt_fbh { return 0 } - return rt_fb[y * rt_fbw + x] + return rt_core_st.rt_fb[y * rt_fbw + x] } # Advance width (pixels) of `text` in the built-in 5x7 font at scale 1: 6 per @@ -300,84 +311,82 @@ function rt_measure_text(text: string) -> int { # ---- camera / clip / blend controls --------------------------------------- # Set the world-space camera offset (a world point (wx,wy) draws at # (wx-x, wy-y)). Reset to (0,0) to draw a fixed HUD. -function rt_camera(x: int, y: int) -> void { rt_cam_x = x; rt_cam_y = y } +function rt_camera(rt_core_st: mut RtCoreState, x: int, y: int) -> void { rt_core_st.rt_cam_x = x; rt_core_st.rt_cam_y = y } # #78 — set the render-time zoom (a Q16.16 scale applied about the screen centre): # 1.0 = no zoom, 2.0 = 2x in, 0.5 = out. Deterministic (fixed-point), so it # preserves lockstep / replay / world_save. Setting exactly 1.0 turns the zoom # path back off, restoring the byte-identical no-zoom blit. -function rt_camera_zoom(scale: fixed) -> void { - rt_cam_zoom = scale - rt_cam_zoomed = scale != 1.0 +function rt_camera_zoom(rt_core_st: mut RtCoreState, scale: fixed) -> void { + rt_core_st.rt_cam_zoom = scale + rt_core_st.rt_cam_zoomed = scale != 1.0 } # Ease the camera so (x,y) drifts toward the screen centre by `lerp` (a fixed in # 0..1): 0 keeps it still, 65536 (1.0) snaps it centred. Deterministic. -function rt_camera_follow(x: int, y: int, lerp: fixed) -> void { +function rt_camera_follow(rt_core_st: mut RtCoreState, x: int, y: int, lerp: fixed) -> void { let tx = x - rt_fbw / 2 let ty = y - rt_fbh / 2 - let sx = fixed(tx - rt_cam_x) * lerp # fixed * fixed, then floor to whole pixels - let sy = fixed(ty - rt_cam_y) * lerp - rt_cam_x += floor(sx) - rt_cam_y += floor(sy) + let sx = fixed(tx - rt_core_st.rt_cam_x) * lerp # fixed * fixed, then floor to whole pixels + let sy = fixed(ty - rt_core_st.rt_cam_y) * lerp + rt_core_st.rt_cam_x += floor(sx) + rt_core_st.rt_cam_y += floor(sy) } # Add a random screen shake of up to +/- amount pixels, drawn from the seeded # RNG (so a replay shakes identically). Call each frame with a decaying amount; # amount <= 0 clears it. -function rt_camera_shake(amount: int) -> void { - if amount <= 0 { rt_shake_x = 0; rt_shake_y = 0; return } - rt_shake_x = rt_rng_range(-amount, amount) - rt_shake_y = rt_rng_range(-amount, amount) +function rt_camera_shake(rt_core_st: mut RtCoreState, rt_rng_st: mut RtRngState, amount: int) -> void { + if amount <= 0 { rt_core_st.rt_shake_x = 0; rt_core_st.rt_shake_y = 0; return } + rt_core_st.rt_shake_x = rt_rng_range(rt_rng_st, -amount, amount) + rt_core_st.rt_shake_y = rt_rng_range(rt_rng_st, -amount, amount) } # Camera.shake_for(amount, frames): shake by up to +/- amount pixels for `frames` # frames, then stop — the engine re-rolls the offset at the start of every frame # (rt_camera_tick, from the frame clear) so no handler has to count it down. A # later call restarts the shake; a bigger amount wins over a smaller one in flight. -var rt_shake_amount: int = 0 -var rt_shake_left: int = 0 -function rt_camera_shake_for(amount: int, frames: int) -> void { - if amount >= rt_shake_amount { rt_shake_amount = amount; rt_shake_left = frames } +function rt_camera_shake_for(rt_core_st: mut RtCoreState, amount: int, frames: int) -> void { + if amount >= rt_core_st.rt_shake_amount { rt_core_st.rt_shake_amount = amount; rt_core_st.rt_shake_left = frames } } -function rt_camera_tick() -> void { - if rt_shake_left > 0 { - rt_shake_left -= 1 - rt_camera_shake(rt_shake_amount) +function rt_camera_tick(rt_core_st: mut RtCoreState, rt_rng_st: mut RtRngState) -> void { + if rt_core_st.rt_shake_left > 0 { + rt_core_st.rt_shake_left -= 1 + rt_camera_shake(rt_core_st, rt_rng_st, rt_core_st.rt_shake_amount) return } - if rt_shake_amount > 0 { rt_shake_amount = 0; rt_camera_shake(0) } + if rt_core_st.rt_shake_amount > 0 { rt_core_st.rt_shake_amount = 0; rt_camera_shake(rt_core_st, rt_rng_st, 0) } } # Restrict drawing to a screen-space rectangle (x, y, width, height). -function rt_clip(x: int, y: int, width: int, height: int) -> void { - rt_clip_x0 = x - rt_clip_y0 = y - rt_clip_x1 = x + width - rt_clip_y1 = y + height +function rt_clip(rt_core_st: mut RtCoreState, x: int, y: int, width: int, height: int) -> void { + rt_core_st.rt_clip_x0 = x + rt_core_st.rt_clip_y0 = y + rt_core_st.rt_clip_x1 = x + width + rt_core_st.rt_clip_y1 = y + height } # Reset the clip rectangle to the whole framebuffer. -function rt_clip_reset() -> void { - rt_clip_x0 = 0 - rt_clip_y0 = 0 - rt_clip_x1 = rt_fbw - rt_clip_y1 = rt_fbh +function rt_clip_reset(rt_core_st: mut RtCoreState) -> void { + rt_core_st.rt_clip_x0 = 0 + rt_core_st.rt_clip_y0 = 0 + rt_core_st.rt_clip_x1 = rt_fbw + rt_core_st.rt_clip_y1 = rt_fbh } # Select the pixel blend mode: 0 = replace (default), 1 = additive. -function rt_blend_mode(mode: int) -> void { rt_blend = mode } +function rt_blend_mode(rt_core_st: mut RtCoreState, mode: int) -> void { rt_core_st.rt_blend = mode } # 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. -function rt_present() -> void { +function rt_present(rt_core_st: RtCoreState) -> void { if is_windowed() { - win_present(rt_fb, rt_fbw, rt_fbh) + win_present(rt_core_st.rt_fb, rt_fbw, rt_fbh) } } # ---- text ----------------------------------------------------------------- -function rt_glyph(x: int, y: int, ch: int, colour: int, sc: int) -> void { +function rt_glyph(rt_core_st: mut RtCoreState, x: int, y: int, ch: int, colour: int, sc: int) -> void { var c = ch if c >= 'a' { if c <= 'z' { c -= 32 } @@ -392,34 +401,34 @@ function rt_glyph(x: int, y: int, ch: int, colour: int, sc: int) -> void { for cc in 0 .. 5 { let on = b / 16 if on == 1 { - rt_fill_rect(x + cc * sc, y + row * sc, sc, sc, colour) + rt_fill_rect(rt_core_st, x + cc * sc, y + row * sc, sc, sc, colour) } b = (b - on * 16) * 2 } } } -function rt_text(x: int, y: int, s: string, colour: int, sc: int) -> void { +function rt_text(rt_core_st: mut RtCoreState, x: int, y: int, s: string, colour: int, sc: int) -> void { var i = 0 var cx = x var ch = s[0] while ch != 0 { - rt_glyph(cx, y, ch, colour, sc) + rt_glyph(rt_core_st, cx, y, ch, colour, sc) cx = cx + 6 * sc i += 1 ch = s[i] } } -function rt_text_int(x: int, y: int, n: int, colour: int, sc: int) -> void { +function rt_text_int(rt_core_st: mut RtCoreState, x: int, y: int, n: int, colour: int, sc: int) -> void { if n == 0 { - rt_glyph(x, y, '0', colour, sc) + rt_glyph(rt_core_st, x, y, '0', colour, sc) return } var v = n var cx = x if v < 0 { - rt_glyph(cx, y, '-', colour, sc) + rt_glyph(rt_core_st, cx, y, '-', colour, sc) cx = cx + 6 * sc v = -v } @@ -435,46 +444,46 @@ function rt_text_int(x: int, y: int, n: int, colour: int, sc: int) -> void { for k in 1 .. p { div *= 10 } - rt_glyph(cx, y, 48 + (v / div) % 10, colour, sc) + rt_glyph(rt_core_st, cx, y, 48 + (v / div) % 10, colour, sc) cx = cx + 6 * sc p -= 1 } } # ---- registers ------------------------------------------------------------ -function rt_reg(i: int) -> int { +function rt_reg(rt_core_st: RtCoreState, i: int) -> int { if i < 0 { return 0 } if i >= 64 { return 0 } - return rt_regs[i] + return rt_core_st.rt_regs[i] } -function rt_set_reg(i: int, v: int) -> void { +function rt_set_reg(rt_core_st: mut RtCoreState, i: int, v: int) -> void { if i < 0 { return } if i >= 64 { return } - rt_regs[i] = v + rt_core_st.rt_regs[i] = v } # ---- platform: input ------------------------------------------------------ -function rt_poll() -> int { +function rt_poll(rt_core_st: mut RtCoreState) -> int { if is_windowed() { return win_poll() } let c = read_char() if c < 0 { - rt_alive = 0 + rt_core_st.rt_alive = 0 return 0 } if c == 'q' { # 'q' quits, as in the headless C platform - rt_alive = 0 + rt_core_st.rt_alive = 0 } return c } -function rt_running() -> bool { +function rt_running(rt_core_st: RtCoreState) -> bool { if is_windowed() { return win_running() } - return rt_alive != 0 + return rt_core_st.rt_alive != 0 } # ---- writing the frame out ------------------------------------------------ @@ -516,7 +525,7 @@ function rt_put_int(buf: pointer, at: int, v: int) -> int { return n } -function rt_dump_ppm(path: string) -> void { +function rt_dump_ppm(rt_core_st: RtCoreState, path: string) -> void { let f = file_open(path, "wb") if (f == null) { return } @@ -531,7 +540,7 @@ function rt_dump_ppm(path: string) -> void { let px = rt_fbw * rt_fbh let buf = bytes(px * 3) for i in 0 .. px { - let c = rt_fb[i] + let c = rt_core_st.rt_fb[i] buf[i * 3] = (c / 65536) % 256 buf[i * 3 + 1] = (c / 256) % 256 buf[i * 3 + 2] = c % 256 @@ -553,91 +562,85 @@ import "grid.ludic" # 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: pointer = null -var rt_mapw: int = 0 -var rt_maph: int = 0 -function rt_map_size(w: int, h: int) -> void { - rt_mapw = clamp(w, 0, 96) - rt_maph = clamp(h, 0, 64) - fill(rt_map, ' ', 96 * 64) +function rt_map_size(rt_core_st: mut RtCoreState, w: int, h: int) -> void { + rt_core_st.rt_mapw = clamp(w, 0, 96) + rt_core_st.rt_maph = clamp(h, 0, 64) + fill(rt_core_st.rt_map, ' ', 96 * 64) } -function rt_map_row(y: int, s: string) -> void { +function rt_map_row(rt_core_st: mut RtCoreState, y: int, s: string) -> void { if y < 0 { return } if y >= 64 { return } var x = 0 var ch = s[0] while ch != 0 { if x >= 96 { return } - rt_map[y * 96 + x] = ch + rt_core_st.rt_map[y * 96 + x] = ch x += 1 ch = s[x] } } # ---- the cell API: a game edits the grid in place instead of keeping its own copy -function rt_map_set(x: int, y: int, glyph: int) -> void { - if (x < 0) or (y < 0) or (x >= rt_mapw) or (y >= rt_maph) { return } - rt_map[y * 96 + x] = glyph +function rt_map_set(rt_core_st: mut RtCoreState, x: int, y: int, glyph: int) -> void { + if (x < 0) or (y < 0) or (x >= rt_core_st.rt_mapw) or (y >= rt_core_st.rt_maph) { return } + rt_core_st.rt_map[y * 96 + x] = glyph } -function rt_map_fill(glyph: int) -> void { +function rt_map_fill(rt_core_st: mut RtCoreState, glyph: int) -> void { var y = 0 - while y < rt_maph { var x = 0; while x < rt_mapw { rt_map[y * 96 + x] = glyph; x += 1 }; y += 1 } + while y < rt_core_st.rt_maph { var x = 0; while x < rt_core_st.rt_mapw { rt_core_st.rt_map[y * 96 + x] = glyph; x += 1 }; y += 1 } } # every cell of the rectangle (x, y, w, h) -function rt_map_rect(x: int, y: int, w: int, h: int, glyph: int) -> void { +function rt_map_rect(rt_core_st: mut RtCoreState, x: int, y: int, w: int, h: int, glyph: int) -> void { var yy = y - while yy < y + h { var xx = x; while xx < x + w { rt_map_set(xx, yy, glyph); xx += 1 }; yy += 1 } + while yy < y + h { var xx = x; while xx < x + w { rt_map_set(rt_core_st, xx, yy, glyph); xx += 1 }; yy += 1 } } # the outermost ring of cells -function rt_map_border(glyph: int) -> void { - rt_map_rect(0, 0, rt_mapw, 1, glyph) - rt_map_rect(0, rt_maph - 1, rt_mapw, 1, glyph) - rt_map_rect(0, 0, 1, rt_maph, glyph) - rt_map_rect(rt_mapw - 1, 0, 1, rt_maph, glyph) +function rt_map_border(rt_core_st: mut RtCoreState, glyph: int) -> void { + rt_map_rect(rt_core_st, 0, 0, rt_core_st.rt_mapw, 1, glyph) + rt_map_rect(rt_core_st, 0, rt_core_st.rt_maph - 1, rt_core_st.rt_mapw, 1, glyph) + rt_map_rect(rt_core_st, 0, 0, 1, rt_core_st.rt_maph, glyph) + rt_map_rect(rt_core_st, rt_core_st.rt_mapw - 1, 0, 1, rt_core_st.rt_maph, glyph) } # a random cell holding `glyph` (seeded RNG): random tries, then a sweep; (-1, -1) if none -function rt_map_random_cell(glyph: int) -> IVec2 { +function rt_map_random_cell(rt_core_st: RtCoreState, rt_rng_st: mut RtRngState, glyph: int) -> IVec2 { var tries = 0 while tries < 64 { - let x = rt_rng_range(0, rt_mapw - 1) - let y = rt_rng_range(0, rt_maph - 1) - if rt_tile(x, y) == glyph { return IVec2.make(x, y) } + let x = rt_rng_range(rt_rng_st, 0, rt_core_st.rt_mapw - 1) + let y = rt_rng_range(rt_rng_st, 0, rt_core_st.rt_maph - 1) + if rt_tile(rt_core_st, x, y) == glyph { return IVec2.make(x, y) } tries += 1 } var y2 = 0 - while y2 < rt_maph { var x2 = 0; while x2 < rt_mapw { if rt_tile(x2, y2) == glyph { return IVec2.make(x2, y2) }; x2 += 1 }; y2 += 1 } + while y2 < rt_core_st.rt_maph { var x2 = 0; while x2 < rt_core_st.rt_mapw { if rt_tile(rt_core_st, x2, y2) == glyph { return IVec2.make(x2, y2) }; x2 += 1 }; y2 += 1 } return IVec2.make(-1, -1) } # a random cell holding `glyph` at least `min_tiles` from `from` (tiles); falls back to any such cell -function rt_map_random_cell_far(glyph: int, from: IVec2, min_tiles: int) -> IVec2 { - var tile = rt_map_random_cell(glyph) +function rt_map_random_cell_far(rt_core_st: RtCoreState, rt_rng_st: mut RtRngState, glyph: int, from: IVec2, min_tiles: int) -> IVec2 { + var tile = rt_map_random_cell(rt_core_st, rt_rng_st, glyph) var tries = 0 while tries < 40 { if not IVec2.within(tile, from, min_tiles - 1) { return tile } - tile = rt_map_random_cell(glyph) + tile = rt_map_random_cell(rt_core_st, rt_rng_st, glyph) tries += 1 } return tile } # the solid glyphs, as the move system read them from the Solids config (0 = none) -var rt_map_solid1: int = 0 -var rt_map_solid2: int = 0 -var rt_map_tile_px: int = 16 -function rt_map_tile_size() -> int { return rt_map_tile_px } -function rt_map_is_solid(x: int, y: int) -> bool { - if rt_map_solid1 == 0 { return false } # no Solids config: nothing is solid - let g = rt_tile(x, y) - if (x < 0) or (y < 0) or (x >= rt_mapw) or (y >= rt_maph) { return true } - if (rt_map_solid1 != 0) and (g == rt_map_solid1) { return true } - if (rt_map_solid2 != 0) and (g == rt_map_solid2) { return true } +function rt_map_tile_size(rt_core_st: RtCoreState) -> int { return rt_core_st.rt_map_tile_px } +function rt_map_is_solid(rt_core_st: RtCoreState, x: int, y: int) -> bool { + if rt_core_st.rt_map_solid1 == 0 { return false } # no Solids config: nothing is solid + let g = rt_tile(rt_core_st, x, y) + if (x < 0) or (y < 0) or (x >= rt_core_st.rt_mapw) or (y >= rt_core_st.rt_maph) { return true } + if (rt_core_st.rt_map_solid1 != 0) and (g == rt_core_st.rt_map_solid1) { return true } + if (rt_core_st.rt_map_solid2 != 0) and (g == rt_core_st.rt_map_solid2) { return true } return false } -function rt_map_is_solid_at(px: int, py: int) -> bool { return rt_map_is_solid(px / rt_map_tile_px, py / rt_map_tile_px) } -function rt_map_to_tile(pixel: IVec2) -> IVec2 { return IVec2.make(pixel.x / rt_map_tile_px, pixel.y / rt_map_tile_px) } -function rt_map_width() -> int { return rt_mapw } -function rt_map_height() -> int { return rt_maph } +function rt_map_is_solid_at(rt_core_st: RtCoreState, px: int, py: int) -> bool { return rt_map_is_solid(rt_core_st, px / rt_core_st.rt_map_tile_px, py / rt_core_st.rt_map_tile_px) } +function rt_map_to_tile(rt_core_st: RtCoreState, pixel: IVec2) -> IVec2 { return IVec2.make(pixel.x / rt_core_st.rt_map_tile_px, pixel.y / rt_core_st.rt_map_tile_px) } +function rt_map_width(rt_core_st: RtCoreState) -> int { return rt_core_st.rt_mapw } +function rt_map_height(rt_core_st: RtCoreState) -> int { return rt_core_st.rt_maph } # IVec2.heading / along / step and Angle.diff_degrees — integer-degree geometry function rt_ivec_heading(a: IVec2, b: IVec2) -> int { @@ -659,22 +662,22 @@ function rt_angle_diff_degrees(a: int, b: int) -> int { } # Screen.bar: a filled meter — `value` of `max` in `color` over a `back` track -function rt_bar(x: int, y: int, w: int, h: int, value: int, max: int, color: int, back: int) -> void { - rt_fill_rect(x, y, w, h, back) +function rt_bar(rt_core_st: mut RtCoreState, x: int, y: int, w: int, h: int, value: int, max: int, color: int, back: int) -> void { + rt_fill_rect(rt_core_st, x, y, w, h, back) var filled = 0 if max > 0 { filled = clamp(value, 0, max) * w / max } - if filled > 0 { rt_fill_rect(x, y, filled, h, color) } + if filled > 0 { rt_fill_rect(rt_core_st, x, y, filled, h, color) } } # List.sample(pool, count): `count` picks from an int slice, distinct while the # pool has enough, repeating a valid pick when it does not; empty in -> zeros -function rt_list_sample(pool: []int, count: int) -> []int { +function rt_list_sample(rt_rng_st: mut RtRngState, pool: []int, count: int) -> []int { let out = new []int let n = len(pool) var i = 0 while i < count { if n == 0 { push(out, 0); i += 1; continue } - var pick = rt_rng_range(0, n - 1) + var pick = rt_rng_range(rt_rng_st, 0, n - 1) var distinct = n > i var tries = 0 while distinct and (tries < 64) { @@ -682,7 +685,7 @@ function rt_list_sample(pool: []int, count: int) -> []int { var j = 0 while j < len(out) { if out[j] == pool[pick] { seen = true }; j += 1 } if not seen { break } - pick = rt_rng_range(0, n - 1) + pick = rt_rng_range(rt_rng_st, 0, n - 1) tries += 1 } push(out, pool[pick]) @@ -691,62 +694,61 @@ function rt_list_sample(pool: []int, count: int) -> []int { return out } -function rt_tile(x: int, y: int) -> int { +function rt_tile(rt_core_st: RtCoreState, 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 rt_map[y * 96 + x] + if x >= rt_core_st.rt_mapw { return 35 } + if y >= rt_core_st.rt_maph { return 35 } + return rt_core_st.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: pointer = null -function rt_status(s: string) -> void { +function rt_status(rt_core_st: mut RtCoreState, s: string) -> void { var i = 0 var ch = s[0] while ch != 0 { if i >= 95 { ch = 0 } if ch != 0 { - rt_statusbuf[i] = ch + rt_core_st.rt_statusbuf[i] = ch i += 1 ch = s[i] } } - rt_statusbuf[i] = 0 + rt_core_st.rt_statusbuf[i] = 0 } -function rt_status_text() -> pointer { - return rt_statusbuf +function rt_status_text(rt_core_st: RtCoreState) -> pointer { + return rt_core_st.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. -function rt_save_state(f: pointer) -> void { +function rt_save_state(rt_core_st: RtCoreState, rt_rng_st: RtRngState, f: pointer) -> void { let w: words = words(4) - w[0] = rt_rng - w[1] = rt_mapw - w[2] = rt_maph - w[3] = rt_alive + w[0] = rt_rng_st.rt_rng + w[1] = rt_core_st.rt_mapw + w[2] = rt_core_st.rt_maph + w[3] = rt_core_st.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) + file_write(f, rt_core_st.rt_regs, 64 * 4) + file_write(f, rt_core_st.rt_map, 96 * 64) + file_write(f, rt_core_st.rt_statusbuf, 96) free(w) } -function rt_load_state(f: pointer) -> void { +function rt_load_state(rt_core_st: mut RtCoreState, rt_rng_st: mut RtRngState, f: pointer) -> void { let w: words = words(4) file_read(f, w, 16) - rt_rng = w[0] - rt_mapw = w[1] - rt_maph = w[2] - file_read(f, rt_regs, 64 * 4) - file_read(f, rt_map, 96 * 64) - file_read(f, rt_statusbuf, 96) + rt_rng_st.rt_rng = w[0] + rt_core_st.rt_mapw = w[1] + rt_core_st.rt_maph = w[2] + file_read(f, rt_core_st.rt_regs, 64 * 4) + file_read(f, rt_core_st.rt_map, 96 * 64) + file_read(f, rt_core_st.rt_statusbuf, 96) free(w) } diff --git a/runtime/native/fx.ludic b/runtime/native/fx.ludic index 2f8b5b3d..a977b8c1 100644 --- a/runtime/native/fx.ludic +++ b/runtime/native/fx.ludic @@ -19,106 +19,108 @@ const FX_SPARK_LIFE_JITTER: int = 8 const FX_SPARK_SPEED: int = 3 # px per frame, each axis, either way const FX_NUMBER_LIFE: int = 30 # frames; a number rises one pixel every other frame -var fx_ready: bool = false -var fx_sx: words = null -var fx_sy: words = null -var fx_svx: words = null -var fx_svy: words = null -var fx_slife: words = null -var fx_scolor: words = null -var fx_ssize: words = null -var fx_sn: int = 0 -var fx_nx: words = null -var fx_ny: words = null -var fx_nvalue: words = null -var fx_nlife: words = null -var fx_ncolor: words = null -var fx_nn: int = 0 - -function fx_init() -> void { - if fx_ready { return } - fx_sx = words(FX_MAX_SPARKS); fx_sy = words(FX_MAX_SPARKS) - fx_svx = words(FX_MAX_SPARKS); fx_svy = words(FX_MAX_SPARKS) - fx_slife = words(FX_MAX_SPARKS); fx_scolor = words(FX_MAX_SPARKS); fx_ssize = words(FX_MAX_SPARKS) - fx_nx = words(FX_MAX_NUMBERS); fx_ny = words(FX_MAX_NUMBERS) - fx_nvalue = words(FX_MAX_NUMBERS); fx_nlife = words(FX_MAX_NUMBERS); fx_ncolor = words(FX_MAX_NUMBERS) - fx_ready = true +export state RtFxState { + fx_ready: bool = false + fx_sx: words = null + fx_sy: words = null + fx_svx: words = null + fx_svy: words = null + fx_slife: words = null + fx_scolor: words = null + fx_ssize: words = null + fx_sn: int = 0 + fx_nx: words = null + fx_ny: words = null + fx_nvalue: words = null + fx_nlife: words = null + fx_ncolor: words = null + fx_nn: int = 0 } -function fx_sparks(x: int, y: int, color: int, count: int) -> void { - fx_init() +function fx_init(rt_fx_st: mut RtFxState) -> void { + if rt_fx_st.fx_ready { return } + rt_fx_st.fx_sx = words(FX_MAX_SPARKS); rt_fx_st.fx_sy = words(FX_MAX_SPARKS) + rt_fx_st.fx_svx = words(FX_MAX_SPARKS); rt_fx_st.fx_svy = words(FX_MAX_SPARKS) + rt_fx_st.fx_slife = words(FX_MAX_SPARKS); rt_fx_st.fx_scolor = words(FX_MAX_SPARKS); rt_fx_st.fx_ssize = words(FX_MAX_SPARKS) + rt_fx_st.fx_nx = words(FX_MAX_NUMBERS); rt_fx_st.fx_ny = words(FX_MAX_NUMBERS) + rt_fx_st.fx_nvalue = words(FX_MAX_NUMBERS); rt_fx_st.fx_nlife = words(FX_MAX_NUMBERS); rt_fx_st.fx_ncolor = words(FX_MAX_NUMBERS) + rt_fx_st.fx_ready = true +} + +function fx_sparks(rt_fx_st: mut RtFxState, rt_rng_st: mut RtRngState, x: int, y: int, color: int, count: int) -> void { + fx_init(rt_fx_st) var i = 0 while i < count { - if fx_sn >= FX_MAX_SPARKS { return } - fx_sx[fx_sn] = x - fx_sy[fx_sn] = y - fx_svx[fx_sn] = rt_rng_range(-FX_SPARK_SPEED, FX_SPARK_SPEED) - fx_svy[fx_sn] = rt_rng_range(-FX_SPARK_SPEED, FX_SPARK_SPEED) - fx_slife[fx_sn] = FX_SPARK_LIFE + rt_rng_range(0, FX_SPARK_LIFE_JITTER) - fx_scolor[fx_sn] = color - fx_ssize[fx_sn] = 1 + rt_rng_range(0, 2) - fx_sn += 1 + if rt_fx_st.fx_sn >= FX_MAX_SPARKS { return } + rt_fx_st.fx_sx[rt_fx_st.fx_sn] = x + rt_fx_st.fx_sy[rt_fx_st.fx_sn] = y + rt_fx_st.fx_svx[rt_fx_st.fx_sn] = rt_rng_range(rt_rng_st, -FX_SPARK_SPEED, FX_SPARK_SPEED) + rt_fx_st.fx_svy[rt_fx_st.fx_sn] = rt_rng_range(rt_rng_st, -FX_SPARK_SPEED, FX_SPARK_SPEED) + rt_fx_st.fx_slife[rt_fx_st.fx_sn] = FX_SPARK_LIFE + rt_rng_range(rt_rng_st, 0, FX_SPARK_LIFE_JITTER) + rt_fx_st.fx_scolor[rt_fx_st.fx_sn] = color + rt_fx_st.fx_ssize[rt_fx_st.fx_sn] = 1 + rt_rng_range(rt_rng_st, 0, 2) + rt_fx_st.fx_sn += 1 i += 1 } } -function fx_number(x: int, y: int, value: int, color: int) -> void { - fx_init() - if fx_nn >= FX_MAX_NUMBERS { return } - fx_nx[fx_nn] = x - fx_ny[fx_nn] = y - fx_nvalue[fx_nn] = value - fx_nlife[fx_nn] = FX_NUMBER_LIFE - fx_ncolor[fx_nn] = color - fx_nn += 1 +function fx_number(rt_fx_st: mut RtFxState, x: int, y: int, value: int, color: int) -> void { + fx_init(rt_fx_st) + if rt_fx_st.fx_nn >= FX_MAX_NUMBERS { return } + rt_fx_st.fx_nx[rt_fx_st.fx_nn] = x + rt_fx_st.fx_ny[rt_fx_st.fx_nn] = y + rt_fx_st.fx_nvalue[rt_fx_st.fx_nn] = value + rt_fx_st.fx_nlife[rt_fx_st.fx_nn] = FX_NUMBER_LIFE + rt_fx_st.fx_ncolor[rt_fx_st.fx_nn] = color + rt_fx_st.fx_nn += 1 } -function fx_clear() -> void { fx_sn = 0; fx_nn = 0 } +function fx_clear(rt_fx_st: mut RtFxState) -> void { rt_fx_st.fx_sn = 0; rt_fx_st.fx_nn = 0 } # drop spark i by moving the last one into its slot -function fx_drop_spark(i: int) -> void { - let last = fx_sn - 1 - fx_sx[i] = fx_sx[last]; fx_sy[i] = fx_sy[last] - fx_svx[i] = fx_svx[last]; fx_svy[i] = fx_svy[last] - fx_slife[i] = fx_slife[last]; fx_scolor[i] = fx_scolor[last]; fx_ssize[i] = fx_ssize[last] - fx_sn = last +function fx_drop_spark(rt_fx_st: mut RtFxState, i: int) -> void { + let last = rt_fx_st.fx_sn - 1 + rt_fx_st.fx_sx[i] = rt_fx_st.fx_sx[last]; rt_fx_st.fx_sy[i] = rt_fx_st.fx_sy[last] + rt_fx_st.fx_svx[i] = rt_fx_st.fx_svx[last]; rt_fx_st.fx_svy[i] = rt_fx_st.fx_svy[last] + rt_fx_st.fx_slife[i] = rt_fx_st.fx_slife[last]; rt_fx_st.fx_scolor[i] = rt_fx_st.fx_scolor[last]; rt_fx_st.fx_ssize[i] = rt_fx_st.fx_ssize[last] + rt_fx_st.fx_sn = last } -function fx_drop_number(i: int) -> void { - let last = fx_nn - 1 - fx_nx[i] = fx_nx[last]; fx_ny[i] = fx_ny[last] - fx_nvalue[i] = fx_nvalue[last]; fx_nlife[i] = fx_nlife[last]; fx_ncolor[i] = fx_ncolor[last] - fx_nn = last +function fx_drop_number(rt_fx_st: mut RtFxState, i: int) -> void { + let last = rt_fx_st.fx_nn - 1 + rt_fx_st.fx_nx[i] = rt_fx_st.fx_nx[last]; rt_fx_st.fx_ny[i] = rt_fx_st.fx_ny[last] + rt_fx_st.fx_nvalue[i] = rt_fx_st.fx_nvalue[last]; rt_fx_st.fx_nlife[i] = rt_fx_st.fx_nlife[last]; rt_fx_st.fx_ncolor[i] = rt_fx_st.fx_ncolor[last] + rt_fx_st.fx_nn = last } # once per Update: move and age everything, dropping what expired -function fx_tick() -> void { - if not fx_ready { return } +function fx_tick(rt_fx_st: mut RtFxState) -> void { + if not rt_fx_st.fx_ready { return } var i = 0 - while i < fx_sn { - fx_sx[i] += fx_svx[i] - fx_sy[i] += fx_svy[i] - fx_slife[i] -= 1 - if fx_slife[i] <= 0 { fx_drop_spark(i) } else { i += 1 } + while i < rt_fx_st.fx_sn { + rt_fx_st.fx_sx[i] += rt_fx_st.fx_svx[i] + rt_fx_st.fx_sy[i] += rt_fx_st.fx_svy[i] + rt_fx_st.fx_slife[i] -= 1 + if rt_fx_st.fx_slife[i] <= 0 { fx_drop_spark(rt_fx_st, i) } else { i += 1 } } i = 0 - while i < fx_nn { - fx_nlife[i] -= 1 - if fx_nlife[i] % 2 == 0 { fx_ny[i] -= 1 } - if fx_nlife[i] <= 0 { fx_drop_number(i) } else { i += 1 } + while i < rt_fx_st.fx_nn { + rt_fx_st.fx_nlife[i] -= 1 + if rt_fx_st.fx_nlife[i] % 2 == 0 { rt_fx_st.fx_ny[i] -= 1 } + if rt_fx_st.fx_nlife[i] <= 0 { fx_drop_number(rt_fx_st, i) } else { i += 1 } } } # once per Render, after the sprites: everything goes through the camera chokepoints -function fx_draw() -> void { - if not fx_ready { return } +function fx_draw(rt_core_st: mut RtCoreState, rt_fx_st: RtFxState) -> void { + if not rt_fx_st.fx_ready { return } var i = 0 - while i < fx_sn { - rt_fill_rect(fx_sx[i], fx_sy[i], fx_ssize[i], fx_ssize[i], fx_scolor[i]) + while i < rt_fx_st.fx_sn { + rt_fill_rect(rt_core_st, rt_fx_st.fx_sx[i], rt_fx_st.fx_sy[i], rt_fx_st.fx_ssize[i], rt_fx_st.fx_ssize[i], rt_fx_st.fx_scolor[i]) i += 1 } i = 0 - while i < fx_nn { - rt_text_int(fx_nx[i] - 4, fx_ny[i] - 10, fx_nvalue[i], fx_ncolor[i], 1) + while i < rt_fx_st.fx_nn { + rt_text_int(rt_core_st, rt_fx_st.fx_nx[i] - 4, rt_fx_st.fx_ny[i] - 10, rt_fx_st.fx_nvalue[i], rt_fx_st.fx_ncolor[i], 1) i += 1 } } diff --git a/runtime/native/gl.ludic b/runtime/native/gl.ludic index 26c40e82..c66d2d59 100644 --- a/runtime/native/gl.ludic +++ b/runtime/native/gl.ludic @@ -73,22 +73,26 @@ extern function f_from_int(a: int) -> int = "f_from_int" extern function f_to_int(a: int) -> int = "f_to_int" # ---- state -------------------------------------------------------------------- -var gl_is_open: bool = false -var gl_w: int = 0 # drawable width, in pixels -var gl_h: int = 0 -var gl_scale: int = 1 # backing pixels per window point -var gl_screen: int = 0 # the framebuffer that is "the screen" (an FBO headless) -var gl_ids: words = null # one-word scratch for glGen*/glGet* +export state RtGlState { + gl_is_open: bool = false + gl_w: int = 0 # drawable width, in pixels + gl_h: int = 0 + gl_scale: int = 1 # backing pixels per window point + gl_screen: int = 0 # the framebuffer that is "the screen" (an FBO headless) + gl_ids: words = null # one-word scratch for glGen*/glGet* + gl_size_buf: words = null + gl_log_buf: string = null +} -function gl_scratch() -> words { - if gl_ids == null { gl_ids = words(4) } - return gl_ids +function gl_scratch(rt_gl_st: mut RtGlState) -> words { + if rt_gl_st.gl_ids == null { rt_gl_st.gl_ids = words(4) } + return rt_gl_st.gl_ids } # Open a GL 4.1 core context on a w x h (points) window titled `title`; headless, # an offscreen context with a w x h framebuffer standing in for the screen. -function gl_open(width: int, height: int, title: pointer) -> bool { - if gl_is_open { return true } +function gl_open(rt_gl_st: mut RtGlState, width: int, height: int, title: pointer) -> bool { + if rt_gl_st.gl_is_open { return true } if is_windowed() { if win_gl_attach() == 0 { win_open(width, height, 1, title) # a plain program: no window yet @@ -99,25 +103,25 @@ function gl_open(width: int, height: int, title: pointer) -> bool { win_set_title(title) } win_gl_resize(width, height) - gl_scale = win_gl_scale() - gl_w = width * gl_scale - gl_h = height * gl_scale - gl_screen = 0 + rt_gl_st.gl_scale = win_gl_scale() + rt_gl_st.gl_w = width * rt_gl_st.gl_scale + rt_gl_st.gl_h = height * rt_gl_st.gl_scale + rt_gl_st.gl_screen = 0 } else { if cgl_offscreen() == 0 { return false } - gl_scale = 1 - gl_w = width - gl_h = height - gl_screen = gl_make_screen_fbo(width, height) + rt_gl_st.gl_scale = 1 + rt_gl_st.gl_w = width + rt_gl_st.gl_h = height + rt_gl_st.gl_screen = gl_make_screen_fbo(rt_gl_st, width, height) } - gl_bind_framebuffer(GL_FRAMEBUFFER, gl_screen) - gl_viewport(0, 0, gl_w, gl_h) - gl_is_open = true + gl_bind_framebuffer(GL_FRAMEBUFFER, rt_gl_st.gl_screen) + gl_viewport(0, 0, rt_gl_st.gl_w, rt_gl_st.gl_h) + rt_gl_st.gl_is_open = true return true } -function gl_make_screen_fbo(w: int, h: int) -> int { - let ids = gl_scratch() +function gl_make_screen_fbo(rt_gl_st: mut RtGlState, w: int, h: int) -> int { + let ids = gl_scratch(rt_gl_st) gl_gen_framebuffers(1, ids) let fbo = ids[0] gl_bind_framebuffer(GL_FRAMEBUFFER, fbo) @@ -138,18 +142,17 @@ function gl_make_screen_fbo(w: int, h: int) -> int { # Did the drawable change size (a window drag, full screen, a Retina switch)? Then # gl_w / gl_h follow it and the caller rebuilds its screen-sized targets. -var gl_size_buf: words = null -function gl_resize_check() -> bool { - if not is_windowed() or not gl_is_open { return false } - if gl_size_buf == null { gl_size_buf = words(4) } - win_gl_drawable(gl_size_buf) - let w = gl_size_buf[0]; let h = gl_size_buf[1] +function gl_resize_check(rt_gl_st: mut RtGlState) -> bool { + if not is_windowed() or not rt_gl_st.gl_is_open { return false } + if rt_gl_st.gl_size_buf == null { rt_gl_st.gl_size_buf = words(4) } + win_gl_drawable(rt_gl_st.gl_size_buf) + let w = rt_gl_st.gl_size_buf[0]; let h = rt_gl_st.gl_size_buf[1] if w <= 0 or h <= 0 { return false } - if w == gl_w and h == gl_h { return false } + if w == rt_gl_st.gl_w and h == rt_gl_st.gl_h { return false } win_gl_update() - gl_w = w; gl_h = h - gl_scale = win_gl_scale() - gl_viewport(0, 0, gl_w, gl_h) + rt_gl_st.gl_w = w; rt_gl_st.gl_h = h + rt_gl_st.gl_scale = win_gl_scale() + gl_viewport(0, 0, rt_gl_st.gl_w, rt_gl_st.gl_h) return true } function gl_set_window(w: int, h: int) -> void { if is_windowed() { win_gl_resize(w, h) } } @@ -157,16 +160,16 @@ function gl_toggle_fullscreen() -> void { if is_windowed() { win_toggle_fullscre function gl_set_retina(on: bool) -> void { if is_windowed() { var v = 0; if on { v = 1 }; win_gl_retina(v) } } # vsync on (1, the default) or off (0); headless has nothing to sync to function gl_vsync(n: int) -> void { if is_windowed() { win_gl_swap_interval(n) } } -function gl_width() -> int { return gl_w } -function gl_height() -> int { return gl_h } -function gl_screen_fbo() -> int { return gl_screen } -function gl_pixel_scale() -> int { return gl_scale } +function gl_width(rt_gl_st: RtGlState) -> int { return rt_gl_st.gl_w } +function gl_height(rt_gl_st: RtGlState) -> int { return rt_gl_st.gl_h } +function gl_screen_fbo(rt_gl_st: RtGlState) -> int { return rt_gl_st.gl_screen } +function gl_pixel_scale(rt_gl_st: RtGlState) -> int { return rt_gl_st.gl_scale } # a renderer that owns its swapchain (render3d's Vulkan path) says what the drawable is -function gl_set_drawable(w: int, h: int) -> void { - gl_w = w - gl_h = h +function gl_set_drawable(rt_gl_st: mut RtGlState, w: int, h: int) -> void { + rt_gl_st.gl_w = w + rt_gl_st.gl_h = h } -function gl_set_pixel_scale(s: int) -> void { gl_scale = s } +function gl_set_pixel_scale(rt_gl_st: mut RtGlState, s: int) -> void { rt_gl_st.gl_scale = s } # Present the frame (vsync'd flushBuffer); headless, just finish the GPU work. function gl_swap() -> void { @@ -175,13 +178,13 @@ function gl_swap() -> void { } # Write what is on the screen framebuffer to a binary PPM (call before Gl.swap). -function gl_screenshot(path: pointer) -> bool { - let w = gl_w - let h = gl_h +function gl_screenshot(rt_gl_st: RtGlState, path: pointer) -> bool { + let w = rt_gl_st.gl_w + let h = rt_gl_st.gl_h let f = file_open(path, "wb") if f == null { return false } let buf = bytes(w * h * 3) - gl_bind_framebuffer(GL_READ_FRAMEBUFFER, gl_screen) + gl_bind_framebuffer(GL_READ_FRAMEBUFFER, rt_gl_st.gl_screen) gl_pixel_storei(GL_PACK_ALIGNMENT, 1) gl_read_pixels(0, 0, w, h, GL_RGB, GL_UNSIGNED_BYTE, buf) let hdr = `P6\n{w} {h}\n255\n` @@ -204,22 +207,21 @@ function gl_check(tag: pointer) -> int { } # ---- shaders -------------------------------------------------------------------- -var gl_log_buf: string = null # Compile one shader stage from source; 0 (and the info log on stdout) on failure. -function gl_shader(kind: int, src: pointer) -> int { +function gl_shader(rt_gl_st: mut RtGlState, kind: int, src: pointer) -> int { let id = gl_create_shader(kind) var srcs: pointers = pointers(1) srcs[0] = src gl_shader_source(id, 1, srcs, null) gl_compile_shader(id) - let ids = gl_scratch() + let ids = gl_scratch(rt_gl_st) gl_get_shaderiv(id, GL_COMPILE_STATUS, ids) if ids[0] == 0 { - if gl_log_buf == null { gl_log_buf = bytes(8192) } - gl_get_shader_info_log(id, 8191, null, gl_log_buf) + if rt_gl_st.gl_log_buf == null { rt_gl_st.gl_log_buf = bytes(8192) } + gl_get_shader_info_log(id, 8191, null, rt_gl_st.gl_log_buf) print("shader compile failed:") - print(gl_log_buf) + print(rt_gl_st.gl_log_buf) gl_delete_shader(id) return 0 } @@ -227,28 +229,28 @@ function gl_shader(kind: int, src: pointer) -> int { } # Link a program from a vertex + fragment source pair; 0 on failure. -function gl_program(vs: pointer, fs: pointer) -> int { - return gl_program5(vs, null, null, null, fs) +function gl_program(rt_gl_st: mut RtGlState, vs: pointer, fs: pointer) -> int { + return gl_program5(rt_gl_st, vs, null, null, null, fs) } # Link a program from up to five stages (null = stage absent). -function gl_program5(vs: pointer, tcs: pointer, tes: pointer, gs: pointer, fs: pointer) -> int { +function gl_program5(rt_gl_st: mut RtGlState, vs: pointer, tcs: pointer, tes: pointer, gs: pointer, fs: pointer) -> int { let prog = gl_create_program() var ok = true - if vs != null { let s = gl_shader(GL_VERTEX_SHADER, vs); if s == 0 { ok = false } else { gl_attach_shader(prog, s) } } - if tcs != null { let s = gl_shader(GL_TESS_CONTROL_SHADER, tcs); if s == 0 { ok = false } else { gl_attach_shader(prog, s) } } - if tes != null { let s = gl_shader(GL_TESS_EVALUATION_SHADER, tes); if s == 0 { ok = false } else { gl_attach_shader(prog, s) } } - if gs != null { let s = gl_shader(GL_GEOMETRY_SHADER, gs); if s == 0 { ok = false } else { gl_attach_shader(prog, s) } } - if fs != null { let s = gl_shader(GL_FRAGMENT_SHADER, fs); if s == 0 { ok = false } else { gl_attach_shader(prog, s) } } + if vs != null { let s = gl_shader(rt_gl_st, GL_VERTEX_SHADER, vs); if s == 0 { ok = false } else { gl_attach_shader(prog, s) } } + if tcs != null { let s = gl_shader(rt_gl_st, GL_TESS_CONTROL_SHADER, tcs); if s == 0 { ok = false } else { gl_attach_shader(prog, s) } } + if tes != null { let s = gl_shader(rt_gl_st, GL_TESS_EVALUATION_SHADER, tes); if s == 0 { ok = false } else { gl_attach_shader(prog, s) } } + if gs != null { let s = gl_shader(rt_gl_st, GL_GEOMETRY_SHADER, gs); if s == 0 { ok = false } else { gl_attach_shader(prog, s) } } + if fs != null { let s = gl_shader(rt_gl_st, GL_FRAGMENT_SHADER, fs); if s == 0 { ok = false } else { gl_attach_shader(prog, s) } } if not ok { gl_delete_program(prog); return 0 } gl_link_program(prog) - let ids = gl_scratch() + let ids = gl_scratch(rt_gl_st) gl_get_programiv(prog, GL_LINK_STATUS, ids) if ids[0] == 0 { - if gl_log_buf == null { gl_log_buf = bytes(8192) } - gl_get_program_info_log(prog, 8191, null, gl_log_buf) + if rt_gl_st.gl_log_buf == null { rt_gl_st.gl_log_buf = bytes(8192) } + gl_get_program_info_log(prog, 8191, null, rt_gl_st.gl_log_buf) print("program link failed:") - print(gl_log_buf) + print(rt_gl_st.gl_log_buf) gl_delete_program(prog) return 0 } @@ -271,24 +273,24 @@ function gl_f32(v: fixed) -> int { return fx_to_f32(v) } function gl_fixed(bits: int) -> fixed { return f32_to_fx(bits) } # One VAO, one VBO helper: create a vertex array object and return it, bound. -function gl_vao() -> int { - let ids = gl_scratch() +function gl_vao(rt_gl_st: mut RtGlState) -> int { + let ids = gl_scratch(rt_gl_st) gl_gen_vertex_arrays(1, ids) gl_bind_vertex_array(ids[0]) return ids[0] } -function gl_buffer() -> int { - let ids = gl_scratch() +function gl_buffer(rt_gl_st: mut RtGlState) -> int { + let ids = gl_scratch(rt_gl_st) gl_gen_buffers(1, ids) return ids[0] } -function gl_texture() -> int { - let ids = gl_scratch() +function gl_texture(rt_gl_st: mut RtGlState) -> int { + let ids = gl_scratch(rt_gl_st) gl_gen_textures(1, ids) return ids[0] } -function gl_framebuffer() -> int { - let ids = gl_scratch() +function gl_framebuffer(rt_gl_st: mut RtGlState) -> int { + let ids = gl_scratch(rt_gl_st) gl_gen_framebuffers(1, ids) return ids[0] } diff --git a/runtime/native/grid.ludic b/runtime/native/grid.ludic index 95986e35..68c36e04 100644 --- a/runtime/native/grid.ludic +++ b/runtime/native/grid.ludic @@ -16,11 +16,11 @@ property Cell { x: int = 0, y: int = 0 } function grid_abs(v: int) -> int { if v < 0 { return -v }; return v } -function grid_in_bounds(x: int, y: int) -> bool { return x >= 0 and y >= 0 and x < rt_mapw and y < rt_maph } +function grid_in_bounds(rt_core_st: RtCoreState, x: int, y: int) -> bool { return x >= 0 and y >= 0 and x < rt_core_st.rt_mapw and y < rt_core_st.rt_maph } # a cell blocks movement if it is out of bounds or holds the `wall` tile. -function grid_blocked(x: int, y: int, wall: int) -> bool { - if not grid_in_bounds(x, y) { return true } - return rt_tile(x, y) == wall +function grid_blocked(rt_core_st: RtCoreState, x: int, y: int, wall: int) -> bool { + if not grid_in_bounds(rt_core_st, x, y) { return true } + return rt_tile(rt_core_st, x, y) == wall } # Bresenham line from (x0,y0) to (x1,y1), inclusive — every cell it crosses. @@ -48,22 +48,22 @@ function grid_line(x0: int, y0: int, x1: int, y1: int) -> []Cell { } # line of sight: true if the straight line hits no `wall` cell (endpoints incl). -function grid_line_of_sight(x0: int, y0: int, x1: int, y1: int, wall: int) -> bool { +function grid_line_of_sight(rt_core_st: RtCoreState, x0: int, y0: int, x1: int, y1: int, wall: int) -> bool { let cells = grid_line(x0, y0, x1, y1) var i = 0 while i < len(cells) { - if grid_blocked(cells[i].x, cells[i].y, wall) { return false } + if grid_blocked(rt_core_st, cells[i].x, cells[i].y, wall) { return false } i += 1 } return true } # 4-connected flood fill: every passable cell reachable from (sx,sy), BFS order. -function grid_flood(sx: int, sy: int, wall: int) -> []Cell { +function grid_flood(rt_core_st: RtCoreState, sx: int, sy: int, wall: int) -> []Cell { let out = new []Cell - if grid_blocked(sx, sy, wall) { return out } - let w = rt_mapw - let n = w * rt_maph + if grid_blocked(rt_core_st, sx, sy, wall) { return out } + let w = rt_core_st.rt_mapw + let n = w * rt_core_st.rt_maph let seen = bytes(n) var i = 0 while i < n { seen[i] = 0; i += 1 } @@ -88,7 +88,7 @@ function grid_flood(sx: int, sy: int, wall: int) -> []Cell { if dir == 1 { nx = cx - 1 } if dir == 2 { ny = cy + 1 } if dir == 3 { ny = cy - 1 } - if grid_in_bounds(nx, ny) and seen[ny * w + nx] == 0 and not grid_blocked(nx, ny, wall) { + if grid_in_bounds(rt_core_st, nx, ny) and seen[ny * w + nx] == 0 and not grid_blocked(rt_core_st, nx, ny, wall) { seen[ny * w + nx] = 1 qx[tail] = nx; qy[tail] = ny; tail += 1 } @@ -102,11 +102,11 @@ function grid_flood(sx: int, sy: int, wall: int) -> []Cell { # heuristic. Returns the path start..goal inclusive, or an empty list if the # goal is unreachable (or start/goal is a wall). The open set is a linear scan — # ample for a tilemap (<= 96x64), and the heuristic keeps it near-optimal work. -function path_a_star(x0: int, y0: int, x1: int, y1: int, wall: int) -> []Cell { +function path_a_star(rt_core_st: RtCoreState, x0: int, y0: int, x1: int, y1: int, wall: int) -> []Cell { let out = new []Cell - if grid_blocked(x0, y0, wall) or grid_blocked(x1, y1, wall) { return out } - let w = rt_mapw - let n = w * rt_maph + if grid_blocked(rt_core_st, x0, y0, wall) or grid_blocked(rt_core_st, x1, y1, wall) { return out } + let w = rt_core_st.rt_mapw + let n = w * rt_core_st.rt_maph let INF = 1000000000 let g = words(n) # cost from start (INF = unvisited) let came = words(n) # parent cell index (-1 = none) @@ -146,7 +146,7 @@ function path_a_star(x0: int, y0: int, x1: int, y1: int, wall: int) -> []Cell { if dir == 1 { nx = cx - 1 } if dir == 2 { ny = cy + 1 } if dir == 3 { ny = cy - 1 } - if grid_in_bounds(nx, ny) and not grid_blocked(nx, ny, wall) { + if grid_in_bounds(rt_core_st, nx, ny) and not grid_blocked(rt_core_st, nx, ny, wall) { let ni = ny * w + nx if closed[ni] == 0 { let ng = g[best] + 1 diff --git a/runtime/native/http.ludic b/runtime/native/http.ludic index 482af12a..e8a2e22c 100644 --- a/runtime/native/http.ludic +++ b/runtime/native/http.ludic @@ -28,50 +28,52 @@ extern function hs_cancel(slot: int) -> void = "hs_cancel" const HTTP_SLOTS: int = 8 -var h_ready: bool = false -var h_used: words = null # slot in use -var h_native: words = null # 1 = backed by the platform fetch, 0 = parsed offline -var h_sent: words = null # 1 = dispatched (native) -var h_res: words = null # 1 = a native fetch has been resolved into the fields below -var h_status: words = null # cached HTTP status (0 = network error / unset) -var h_blen: words = null # body length -var h_req: pointers = null # the pending NSURLRequest (native, before send) -var h_body: pointers = null # body bytes (NUL-terminated); owned by the slot -var h_hdr: pointers = null # raw header block (parsed handles only); owned by the slot -var h_save: words = null # 1 = the body streams to a file (Http.save_to) - -function http_init() -> void { - if h_ready { return } - h_used = words(HTTP_SLOTS) - h_native = words(HTTP_SLOTS) - h_sent = words(HTTP_SLOTS) - h_res = words(HTTP_SLOTS) - h_status = words(HTTP_SLOTS) - h_blen = words(HTTP_SLOTS) - h_req = pointers(HTTP_SLOTS) - h_body = pointers(HTTP_SLOTS) - h_hdr = pointers(HTTP_SLOTS) - h_save = words(HTTP_SLOTS) - var i = 0 - while i < HTTP_SLOTS { - h_used[i] = 0; h_native[i] = 0; h_sent[i] = 0; h_res[i] = 0 - h_status[i] = 0; h_blen[i] = 0 - h_req[i] = null; h_body[i] = null; h_hdr[i] = null - h_save[i] = 0 - i += 1 - } - h_ready = true +export state RtHttpState { + h_ready: bool = false + h_used: words = null # slot in use + h_native: words = null # 1 = backed by the platform fetch, 0 = parsed offline + h_sent: words = null # 1 = dispatched (native) + h_res: words = null # 1 = a native fetch has been resolved into the fields below + h_status: words = null # cached HTTP status (0 = network error / unset) + h_blen: words = null # body length + h_req: pointers = null # the pending NSURLRequest (native, before send) + h_body: pointers = null # body bytes (NUL-terminated); owned by the slot + h_hdr: pointers = null # raw header block (parsed handles only); owned by the slot + h_save: words = null # 1 = the body streams to a file (Http.save_to) } -function http_slot_alloc() -> int { - http_init() +function http_init(rt_http_st: mut RtHttpState) -> void { + if rt_http_st.h_ready { return } + rt_http_st.h_used = words(HTTP_SLOTS) + rt_http_st.h_native = words(HTTP_SLOTS) + rt_http_st.h_sent = words(HTTP_SLOTS) + rt_http_st.h_res = words(HTTP_SLOTS) + rt_http_st.h_status = words(HTTP_SLOTS) + rt_http_st.h_blen = words(HTTP_SLOTS) + rt_http_st.h_req = pointers(HTTP_SLOTS) + rt_http_st.h_body = pointers(HTTP_SLOTS) + rt_http_st.h_hdr = pointers(HTTP_SLOTS) + rt_http_st.h_save = words(HTTP_SLOTS) var i = 0 while i < HTTP_SLOTS { - if h_used[i] == 0 { - h_used[i] = 1; h_native[i] = 0; h_sent[i] = 0; h_res[i] = 0 - h_status[i] = 0; h_blen[i] = 0 - h_req[i] = null; h_body[i] = null; h_hdr[i] = null - h_save[i] = 0 + rt_http_st.h_used[i] = 0; rt_http_st.h_native[i] = 0; rt_http_st.h_sent[i] = 0; rt_http_st.h_res[i] = 0 + rt_http_st.h_status[i] = 0; rt_http_st.h_blen[i] = 0 + rt_http_st.h_req[i] = null; rt_http_st.h_body[i] = null; rt_http_st.h_hdr[i] = null + rt_http_st.h_save[i] = 0 + i += 1 + } + rt_http_st.h_ready = true +} + +function http_slot_alloc(rt_http_st: mut RtHttpState) -> int { + http_init(rt_http_st) + var i = 0 + while i < HTTP_SLOTS { + if rt_http_st.h_used[i] == 0 { + rt_http_st.h_used[i] = 1; rt_http_st.h_native[i] = 0; rt_http_st.h_sent[i] = 0; rt_http_st.h_res[i] = 0 + rt_http_st.h_status[i] = 0; rt_http_st.h_blen[i] = 0 + rt_http_st.h_req[i] = null; rt_http_st.h_body[i] = null; rt_http_st.h_hdr[i] = null + rt_http_st.h_save[i] = 0 return i } i += 1 @@ -79,22 +81,22 @@ function http_slot_alloc() -> int { return -1 } -function http_valid(h: int) -> bool { - http_init() +function http_valid(rt_http_st: mut RtHttpState, h: int) -> bool { + http_init(rt_http_st) if (h < 1) or (h > HTTP_SLOTS) { return false } - return h_used[h - 1] == 1 + return rt_http_st.h_used[h - 1] == 1 } # A native request freed while its worker was still running is parked (h_used 2) rather # than released, so the worker never writes into a slot a new request has taken. Once the # worker has finished, the slot's native resources are freed and it is free again. -function http_reap() -> void { - http_init() +function http_reap(rt_http_st: mut RtHttpState) -> void { + http_init(rt_http_st) var i = 0 while i < HTTP_SLOTS { - if (h_used[i] == 2) and (hs_done(i) != 0) { + if (rt_http_st.h_used[i] == 2) and (hs_done(i) != 0) { hs_free(i) - h_used[i] = 0 + rt_http_st.h_used[i] = 0 } i += 1 } @@ -110,32 +112,32 @@ function http_cstr_len(p: pointer) -> int { # ---- request construction (native) ----------------------------------------- # Open a request without sending it, so headers / a body can be added first. -function http_open(method: pointer, url: pointer) -> int { - http_reap() +function http_open(rt_http_st: mut RtHttpState, method: pointer, url: pointer) -> int { + http_reap(rt_http_st) let req = hs_req_new(method, url) if req == null { return 0 } - let s = http_slot_alloc() + let s = http_slot_alloc(rt_http_st) if s < 0 { return 0 } - h_req[s] = req - h_native[s] = 1 + rt_http_st.h_req[s] = req + rt_http_st.h_native[s] = 1 return s + 1 } -function http_set_header(h: int, name: pointer, value: pointer) -> void { - if not http_valid(h) { return } +function http_set_header(rt_http_st: mut RtHttpState, h: int, name: pointer, value: pointer) -> void { + if not http_valid(rt_http_st, h) { return } let s = h - 1 - if (h_sent[s] == 0) and (h_req[s] != null) { hs_req_header(h_req[s], name, value) } + if (rt_http_st.h_sent[s] == 0) and (rt_http_st.h_req[s] != null) { hs_req_header(rt_http_st.h_req[s], name, value) } } # Attach a body of `len` bytes (use http_body for a NUL-terminated string body). -function http_body_n(h: int, bytes_ptr: pointer, len: int) -> void { - if not http_valid(h) { return } +function http_body_n(rt_http_st: mut RtHttpState, h: int, bytes_ptr: pointer, len: int) -> void { + if not http_valid(rt_http_st, h) { return } let s = h - 1 - if (h_sent[s] == 0) and (h_req[s] != null) { hs_req_body(h_req[s], bytes_ptr, len) } + if (rt_http_st.h_sent[s] == 0) and (rt_http_st.h_req[s] != null) { hs_req_body(rt_http_st.h_req[s], bytes_ptr, len) } } -function http_body(h: int, body: pointer) -> void { - http_body_n(h, body, http_cstr_len(body)) +function http_body(rt_http_st: mut RtHttpState, h: int, body: pointer) -> void { + http_body_n(rt_http_st, h, body, http_cstr_len(body)) } # Stream the response body to the file at `path` instead of keeping it in memory. Must be @@ -143,134 +145,134 @@ function http_body(h: int, body: pointer) -> void { # and written directly - no temporary name - so a failed or cancelled download leaves # whatever arrived there; the caller picks the name and renames it once the status is # good. Http.text of such a request is empty (null) and Http.body_len is the bytes written. -function http_save_to(h: int, path: pointer) -> void { - if not http_valid(h) { return } +function http_save_to(rt_http_st: mut RtHttpState, h: int, path: pointer) -> void { + if not http_valid(rt_http_st, h) { return } let s = h - 1 - if (h_native[s] == 1) and (h_sent[s] == 0) and (path != null) { + if (rt_http_st.h_native[s] == 1) and (rt_http_st.h_sent[s] == 0) and (path != null) { hs_save_to(s, path) - h_save[s] = 1 + rt_http_st.h_save[s] = 1 } } # Body bytes received so far. Live while a Http.save_to request is pending; for an # in-memory request it is 0 until the reply lands, then the body length. -function http_received(h: int) -> int { - if not http_valid(h) { return 0 } +function http_received(rt_http_st: mut RtHttpState, h: int) -> int { + if not http_valid(rt_http_st, h) { return 0 } let s = h - 1 - if h_native[s] == 0 { return h_blen[s] } - if h_sent[s] == 0 { return 0 } - if h_save[s] == 1 { return hs_received(s) } - if h_res[s] == 1 { return h_blen[s] } + if rt_http_st.h_native[s] == 0 { return rt_http_st.h_blen[s] } + if rt_http_st.h_sent[s] == 0 { return 0 } + if rt_http_st.h_save[s] == 1 { return hs_received(s) } + if rt_http_st.h_res[s] == 1 { return rt_http_st.h_blen[s] } return 0 } # The body's announced length (Content-Length), or -1 while it is not known. For a # Http.save_to request it is known as soon as the headers arrive; for an in-memory request # it is -1 until the reply lands, then the body length. -function http_expected(h: int) -> int { - if not http_valid(h) { return -1 } +function http_expected(rt_http_st: mut RtHttpState, h: int) -> int { + if not http_valid(rt_http_st, h) { return -1 } let s = h - 1 - if h_native[s] == 0 { return h_blen[s] } - if h_sent[s] == 0 { return -1 } - if h_save[s] == 1 { return hs_expected(s) } - if h_res[s] == 1 { return h_blen[s] } + if rt_http_st.h_native[s] == 0 { return rt_http_st.h_blen[s] } + if rt_http_st.h_sent[s] == 0 { return -1 } + if rt_http_st.h_save[s] == 1 { return hs_expected(s) } + if rt_http_st.h_res[s] == 1 { return rt_http_st.h_blen[s] } return -1 } # Dispatch the request onto the background worker. -function http_send_req(h: int) -> void { - if not http_valid(h) { return } +function http_send_req(rt_http_st: mut RtHttpState, h: int) -> void { + if not http_valid(rt_http_st, h) { return } let s = h - 1 - if (h_sent[s] == 0) and (h_req[s] != null) { - hs_send(s, h_req[s]) - h_sent[s] = 1 + if (rt_http_st.h_sent[s] == 0) and (rt_http_st.h_req[s] != null) { + hs_send(s, rt_http_st.h_req[s]) + rt_http_st.h_sent[s] = 1 } } # ---- convenience one-shots ------------------------------------------------- -function http_get(url: pointer) -> int { - let h = http_open("GET", url) - if h != 0 { http_send_req(h) } +function http_get(rt_http_st: mut RtHttpState, url: pointer) -> int { + let h = http_open(rt_http_st, "GET", url) + if h != 0 { http_send_req(rt_http_st, h) } return h } -function http_post(url: pointer, body: pointer) -> int { - let h = http_open("POST", url) - if h != 0 { http_body(h, body); http_send_req(h) } +function http_post(rt_http_st: mut RtHttpState, url: pointer, body: pointer) -> int { + let h = http_open(rt_http_st, "POST", url) + if h != 0 { http_body(rt_http_st, h, body); http_send_req(rt_http_st, h) } return h } -function http_request(method: pointer, url: pointer, body: pointer) -> int { - let h = http_open(method, url) +function http_request(rt_http_st: mut RtHttpState, method: pointer, url: pointer, body: pointer) -> int { + let h = http_open(rt_http_st, method, url) if h == 0 { return 0 } - if http_cstr_len(body) > 0 { http_body(h, body) } - http_send_req(h) + if http_cstr_len(body) > 0 { http_body(rt_http_st, h, body) } + http_send_req(rt_http_st, h) return h } # ---- polling + response ---------------------------------------------------- # -1 while pending, 0 on a transport error, else the HTTP status code. -function http_poll(h: int) -> int { - if not http_valid(h) { return 0 } +function http_poll(rt_http_st: mut RtHttpState, h: int) -> int { + if not http_valid(rt_http_st, h) { return 0 } let s = h - 1 - if h_native[s] == 0 { return h_status[s] } # a parsed handle is ready immediately - if h_sent[s] == 0 { return -1 } - if h_res[s] == 1 { return h_status[s] } + if rt_http_st.h_native[s] == 0 { return rt_http_st.h_status[s] } # a parsed handle is ready immediately + if rt_http_st.h_sent[s] == 0 { return -1 } + if rt_http_st.h_res[s] == 1 { return rt_http_st.h_status[s] } if hs_done(s) == 0 { return -1 } # first time we see it finished: cache the fields off the worker. - h_status[s] = hs_status(s) - h_body[s] = hs_body(s) - h_blen[s] = hs_blen(s) - h_res[s] = 1 - return h_status[s] + rt_http_st.h_status[s] = hs_status(s) + rt_http_st.h_body[s] = hs_body(s) + rt_http_st.h_blen[s] = hs_blen(s) + rt_http_st.h_res[s] = 1 + return rt_http_st.h_status[s] } -function http_status_of(h: int) -> int { - if not http_valid(h) { return 0 } - return h_status[h - 1] +function http_status_of(rt_http_st: mut RtHttpState, h: int) -> int { + if not http_valid(rt_http_st, h) { return 0 } + return rt_http_st.h_status[h - 1] } -function http_ok(h: int) -> bool { - let st = http_status_of(h) +function http_ok(rt_http_st: mut RtHttpState, h: int) -> bool { + let st = http_status_of(rt_http_st, h) return (st >= 200) and (st < 300) } -function http_text(h: int) -> string { - if not http_valid(h) { return null } - return h_body[h - 1] +function http_text(rt_http_st: mut RtHttpState, h: int) -> string { + if not http_valid(rt_http_st, h) { return null } + return rt_http_st.h_body[h - 1] } -function http_body_len(h: int) -> int { - if not http_valid(h) { return 0 } - return h_blen[h - 1] +function http_body_len(rt_http_st: mut RtHttpState, h: int) -> int { + if not http_valid(rt_http_st, h) { return 0 } + return rt_http_st.h_blen[h - 1] } -function http_header_of(h: int, name: pointer) -> string { - if not http_valid(h) { return null } +function http_header_of(rt_http_st: mut RtHttpState, h: int, name: pointer) -> string { + if not http_valid(rt_http_st, h) { return null } let s = h - 1 - if h_native[s] == 1 { return hs_header(s, name) } - return http_find_header(h_hdr[s], name) + if rt_http_st.h_native[s] == 1 { return hs_header(s, name) } + return http_find_header(rt_http_st.h_hdr[s], name) } -function http_close(h: int) -> void { - if not http_valid(h) { return } +function http_close(rt_http_st: mut RtHttpState, h: int) -> void { + if not http_valid(rt_http_st, h) { return } let s = h - 1 - if h_native[s] == 1 { - if (h_sent[s] == 1) and (h_res[s] == 0) and (hs_done(s) == 0) { + if rt_http_st.h_native[s] == 1 { + if (rt_http_st.h_sent[s] == 1) and (rt_http_st.h_res[s] == 0) and (hs_done(s) == 0) { # still running: stop it, and park the slot until the worker has let go hs_cancel(s) - h_used[s] = 2 - h_req[s] = null; h_body[s] = null; h_hdr[s] = null + rt_http_st.h_used[s] = 2 + rt_http_st.h_req[s] = null; rt_http_st.h_body[s] = null; rt_http_st.h_hdr[s] = null return } hs_free(s) } else { - if h_body[s] != null { free(h_body[s]) } - if h_hdr[s] != null { free(h_hdr[s]) } + if rt_http_st.h_body[s] != null { free(rt_http_st.h_body[s]) } + if rt_http_st.h_hdr[s] != null { free(rt_http_st.h_hdr[s]) } } - h_used[s] = 0 - h_req[s] = null; h_body[s] = null; h_hdr[s] = null + rt_http_st.h_used[s] = 0 + rt_http_st.h_req[s] = null; rt_http_st.h_body[s] = null; rt_http_st.h_hdr[s] = null } # ---- the pure-Ludic response parser (transport-independent) ----------------- @@ -290,10 +292,10 @@ function http_lower(c: int) -> int { if (c >= 'A') and (c <= 'Z') { return c + 3 # Parse a raw HTTP/1.1 response (status line + headers + blank line + body) into # a ready handle. Useful for cached/custom transports and offline tests. -function http_parse(resp: pointer, len: int) -> int { - let s = http_slot_alloc() +function http_parse(rt_http_st: mut RtHttpState, resp: pointer, len: int) -> int { + let s = http_slot_alloc(rt_http_st) if s < 0 { return 0 } - h_native[s] = 0 + rt_http_st.h_native[s] = 0 # status: the token after the first space on the status line. var i = 0 @@ -301,7 +303,7 @@ function http_parse(resp: pointer, len: int) -> int { while (i < len) and (resp[i] == ' ') { i += 1 } # skip the space(s) var st = 0 while (i < len) and http_is_digit(resp[i]) { st = (st * 10) + (resp[i] - 48); i += 1 } - h_status[s] = st + rt_http_st.h_status[s] = st # header/body split at the first CRLFCRLF. var split = -1 @@ -323,10 +325,10 @@ function http_parse(resp: pointer, len: int) -> int { hs += 1 } if hs > hdr_end { hs = hdr_end } - h_hdr[s] = http_slice_dup(resp, hs, hdr_end) - h_body[s] = http_slice_dup(resp, body_start, len) - h_blen[s] = len - body_start - if h_blen[s] < 0 { h_blen[s] = 0 } + rt_http_st.h_hdr[s] = http_slice_dup(resp, hs, hdr_end) + rt_http_st.h_body[s] = http_slice_dup(resp, body_start, len) + rt_http_st.h_blen[s] = len - body_start + if rt_http_st.h_blen[s] < 0 { rt_http_st.h_blen[s] = 0 } return s + 1 } diff --git a/runtime/native/image.ludic b/runtime/native/image.ludic index 593a7c7c..0960efd9 100644 --- a/runtime/native/image.ludic +++ b/runtime/native/image.ludic @@ -13,25 +13,31 @@ const IMG_MAX: int = 32 const SPR_MAX: int = 160 const SPR_SZ: int = 16 -var img_px: [][]int = null # IMG_MAX RGBA buffers -var img_w: words = null -var img_h: words = null -var img_n: int = 0 +export state RtImageState { + img_px: [][]int = null # IMG_MAX RGBA buffers + img_w: words = null + img_h: words = null + img_n: int = 0 + spr_px: words = null # SPR_MAX * 16 * 16 RGBA pixels, one block + spr_n: int = 0 + rt_file_len: int = 0 + png_w: int = 0 + png_h: int = 0 + png_px: words = null +} -var spr_px: words = null # SPR_MAX * 16 * 16 RGBA pixels, one block -var spr_n: int = 0 -function rt_image_init() -> void { - img_px = new [][]int +function rt_image_init(rt_image_st: mut RtImageState) -> void { + rt_image_st.img_px = new [][]int var pi0 = 0 while pi0 < IMG_MAX { - push(img_px, null) + push(rt_image_st.img_px, null) pi0 += 1 } - img_w = words(IMG_MAX) - img_h = words(IMG_MAX) - spr_px = words(SPR_MAX * SPR_SZ * SPR_SZ) - fill(spr_px, 0, SPR_MAX * SPR_SZ * SPR_SZ * 4) + rt_image_st.img_w = words(IMG_MAX) + rt_image_st.img_h = words(IMG_MAX) + rt_image_st.spr_px = words(SPR_MAX * SPR_SZ * SPR_SZ) + fill(rt_image_st.spr_px, 0, SPR_MAX * SPR_SZ * SPR_SZ * 4) } # ---- decoding ------------------------------------------------------------- @@ -47,7 +53,7 @@ function png_tag(b: pointer, at: int, a: int, c: int, d: int, e: int) -> bool { return true } -function rt_read_file(path: string) -> pointer { +function rt_read_file(rt_image_st: mut RtImageState, path: string) -> pointer { let f = file_open(path, "rb") if (f == null) { return null } file_seek(f, 0, 2) @@ -57,23 +63,19 @@ function rt_read_file(path: string) -> pointer { let buf = bytes(n + 8) file_read(f, buf, n) file_close(f) - rt_file_len = n + rt_image_st.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: words = null -function rt_decode_png(path: string) -> bool { - png_w = 0 - png_h = 0 - png_px = null - let d = rt_read_file(path) +function rt_decode_png(rt_image_st: mut RtImageState, rt_inflate_st: mut RtInflateState, path: string) -> bool { + rt_image_st.png_w = 0 + rt_image_st.png_h = 0 + rt_image_st.png_px = null + let d = rt_read_file(rt_image_st, path) if (d == null) { return false } - let size = rt_file_len + let size = rt_image_st.rt_file_len if size < 8 { free(d); return false } if d[0] != 137 { free(d); return false } if d[1] != 'P' { free(d); return false } @@ -136,7 +138,7 @@ function rt_decode_png(path: string) -> bool { let stride = (w * bppbits + 7) / 8 let rawlen = h * (stride + 1) let raw = bytes(rawlen + 8) - if z_uncompress(idat, idlen, raw, rawlen) < 0 { + if z_uncompress(rt_inflate_st, idat, idlen, raw, rawlen) < 0 { free(d); free(raw); free(idat) return false } @@ -225,25 +227,25 @@ function rt_decode_png(path: string) -> bool { free(raw) free(plte) free(trns) - png_w = w - png_h = h - png_px = out + rt_image_st.png_w = w + rt_image_st.png_h = h + rt_image_st.png_px = out return true } # ---- images --------------------------------------------------------------- -function rt_image_load(path: string) -> int { - if img_n >= IMG_MAX { return -1 } - if rt_decode_png(path) == false { return -1 } - let id = img_n - img_n += 1 - img_px[id] = png_px - img_w[id] = png_w - img_h[id] = png_h +function rt_image_load(rt_image_st: mut RtImageState, rt_inflate_st: mut RtInflateState, path: string) -> int { + if rt_image_st.img_n >= IMG_MAX { return -1 } + if rt_decode_png(rt_image_st, rt_inflate_st, path) == false { return -1 } + let id = rt_image_st.img_n + rt_image_st.img_n += 1 + rt_image_st.img_px[id] = rt_image_st.png_px + rt_image_st.img_w[id] = rt_image_st.png_w + rt_image_st.img_h[id] = rt_image_st.png_h return id } -function rt_blend_px(x: int, y: int, argb: int) -> void { +function rt_blend_px(rt_core_st: mut RtCoreState, x: int, y: int, argb: int) -> void { let a = ((argb >> 24) & 255) if a == 0 { return } if x < 0 { return } @@ -254,7 +256,7 @@ function rt_blend_px(x: int, y: int, argb: int) -> void { var g = ((argb >> 8) & 255) var b = (argb & 255) if a != 255 { - let dst = rt_fb[y * rt_fbw + x] + let dst = rt_core_st.rt_fb[y * rt_fbw + x] let dr = ((dst >> 16) & 255) let dg = ((dst >> 8) & 255) let db = (dst & 255) @@ -262,33 +264,33 @@ function rt_blend_px(x: int, y: int, argb: int) -> void { g = (g * a + dg * (255 - a)) / 255 b = (b * a + db * (255 - a)) / 255 } - rt_fb[y * rt_fbw + x] = (((r << 16) | (g << 8)) | b) + rt_core_st.rt_fb[y * rt_fbw + x] = (((r << 16) | (g << 8)) | b) } -function rt_draw_image(id: int, dx: int, dy: int) -> void { +function rt_draw_image(rt_core_st: mut RtCoreState, rt_image_st: RtImageState, id: int, dx: int, dy: int) -> void { if id < 0 { return } - if id >= img_n { return } - let w = img_w[id] - let h = img_h[id] - let s: words = img_px[id] + if id >= rt_image_st.img_n { return } + let w = rt_image_st.img_w[id] + let h = rt_image_st.img_h[id] + let s: words = rt_image_st.img_px[id] for y in 0 .. h { for x in 0 .. w { - rt_blend_px(dx + x, dy + y, s[y * w + x]) + rt_blend_px(rt_core_st, dx + x, dy + y, s[y * w + x]) } } } -function rt_draw_image_scaled(id: int, dx: int, dy: int, dw: int, dh: int) -> void { +function rt_draw_image_scaled(rt_core_st: mut RtCoreState, rt_image_st: RtImageState, id: int, dx: int, dy: int, dw: int, dh: int) -> void { if id < 0 { return } - if id >= img_n { return } + if id >= rt_image_st.img_n { return } if dw <= 0 { return } if dh <= 0 { return } - let w = img_w[id] - let h = img_h[id] - let s: words = img_px[id] + let w = rt_image_st.img_w[id] + let h = rt_image_st.img_h[id] + let s: words = rt_image_st.img_px[id] for y in 0 .. dh { for x in 0 .. dw { - rt_blend_px(dx + x, dy + y, s[(y * h / dh) * w + x * w / dw]) + rt_blend_px(rt_core_st, dx + x, dy + y, s[(y * h / dh) * w + x * w / dw]) } } } @@ -303,70 +305,70 @@ function rt_9map(d: int, dsz: int, ssz: int, inset: int) -> int { return inset + (d - inset) * (ssz - 2 * inset) / (dsz - 2 * inset) } -function rt_draw_9slice(id: int, dx: int, dy: int, dw: int, dh: int, inset: int) -> void { +function rt_draw_9slice(rt_core_st: mut RtCoreState, rt_image_st: RtImageState, id: int, dx: int, dy: int, dw: int, dh: int, inset: int) -> void { if id < 0 { return } - if id >= img_n { return } + if id >= rt_image_st.img_n { return } if dw <= 0 { return } if dh <= 0 { return } - let w = img_w[id] - let h = img_h[id] - let s: words = img_px[id] + let w = rt_image_st.img_w[id] + let h = rt_image_st.img_h[id] + let s: words = rt_image_st.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, s[sy * w + rt_9map(x, dw, w, inset)]) + rt_blend_px(rt_core_st, dx + x, dy + y, s[sy * w + rt_9map(x, dw, w, inset)]) } } } # ---- sprites (16x16 art) -------------------------------------------------- -function rt_png_load(path: string) -> int { - if spr_n >= SPR_MAX { return -1 } - if rt_decode_png(path) == false { return -1 } - let id = spr_n - spr_n += 1 +function rt_png_load(rt_image_st: mut RtImageState, rt_inflate_st: mut RtInflateState, path: string) -> int { + if rt_image_st.spr_n >= SPR_MAX { return -1 } + if rt_decode_png(rt_image_st, rt_inflate_st, path) == false { return -1 } + let id = rt_image_st.spr_n + rt_image_st.spr_n += 1 let base = id * SPR_SZ * SPR_SZ for y in 0 .. SPR_SZ { for x in 0 .. SPR_SZ { var px = 0 - if x < png_w { - if y < png_h { - let c = png_px[y * png_w + x] + if x < rt_image_st.png_w { + if y < rt_image_st.png_h { + let c = rt_image_st.png_px[y * rt_image_st.png_w + x] if ((c >> 24) & 255) >= 128 { px = ((255 << 24) | (c & 16777215)) } } } - spr_px[base + y * SPR_SZ + x] = px + rt_image_st.spr_px[base + y * SPR_SZ + x] = px } } - free(png_px) - png_px = null + free(rt_image_st.png_px) + rt_image_st.png_px = null return id } -function rt_draw_sprite(id: int, px: int, py: int) -> void { +function rt_draw_sprite(rt_core_st: mut RtCoreState, rt_image_st: RtImageState, id: int, px: int, py: int) -> void { if id < 0 { return } - if id >= spr_n { return } + if id >= rt_image_st.spr_n { return } let base = id * SPR_SZ * SPR_SZ for y in 0 .. SPR_SZ { for x in 0 .. SPR_SZ { - let p = spr_px[base + y * SPR_SZ + x] + let p = rt_image_st.spr_px[base + y * SPR_SZ + x] if ((p >> 24) & 255) != 0 { - rt_put_px(px + x, py + y, (p & 16777215)) + rt_put_px(rt_core_st, px + x, py + y, (p & 16777215)) } } } } -function rt_draw_sprite_scaled(id: int, px: int, py: int, sc: int) -> void { +function rt_draw_sprite_scaled(rt_core_st: mut RtCoreState, rt_image_st: RtImageState, id: int, px: int, py: int, sc: int) -> void { if id < 0 { return } - if id >= spr_n { return } + if id >= rt_image_st.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 = spr_px[base + y * SPR_SZ + x] + let p = rt_image_st.spr_px[base + y * SPR_SZ + x] if ((p >> 24) & 255) != 0 { - rt_fill_rect(px + x * sc, py + y * sc, sc, sc, (p & 16777215)) + rt_fill_rect(rt_core_st, px + x * sc, py + y * sc, sc, sc, (p & 16777215)) } } } @@ -377,9 +379,9 @@ function rt_draw_sprite_scaled(id: int, px: int, py: int, sc: int) -> void { # sprite's own colours; non-zero = draw every opaque pixel in the tint colour, for # a hit-flash / team-colour silhouette). It funnels through the same put_px / # fill_rect chokepoints, so camera / zoom / clip / blend all apply. -function rt_draw_sprite_ex(id: int, px: int, py: int, sc: int, flip: int, tint: int) -> void { +function rt_draw_sprite_ex(rt_core_st: mut RtCoreState, rt_image_st: RtImageState, id: int, px: int, py: int, sc: int, flip: int, tint: int) -> void { if id < 0 { return } - if id >= spr_n { return } + if id >= rt_image_st.spr_n { return } var s = sc if s < 1 { s = 1 } let base = id * SPR_SZ * SPR_SZ @@ -387,12 +389,12 @@ function rt_draw_sprite_ex(id: int, px: int, py: int, sc: int, flip: int, tint: for x in 0 .. SPR_SZ { var rx = x if flip != 0 { rx = SPR_SZ - 1 - x } # sample mirrored, draw upright - let p = spr_px[base + y * SPR_SZ + rx] + let p = rt_image_st.spr_px[base + y * SPR_SZ + rx] if ((p >> 24) & 255) != 0 { var col = (p & 16777215) if tint != 0 { col = (tint & 16777215) } - if s == 1 { rt_put_px(px + x, py + y, col) } - else { rt_fill_rect(px + x * s, py + y * s, s, s, col) } + if s == 1 { rt_put_px(rt_core_st, px + x, py + y, col) } + else { rt_fill_rect(rt_core_st, px + x * s, py + y * s, s, s, col) } } } } @@ -406,10 +408,10 @@ function rt_hexval(c: int) -> int { return -1 } -function rt_sprites_load(path: string) -> void { - let d = rt_read_file(path) - if (d == null) { spr_n = 0; return } - let size = rt_file_len +function rt_sprites_load(rt_image_st: mut RtImageState, path: string) -> void { + let d = rt_read_file(rt_image_st, path) + if (d == null) { rt_image_st.spr_n = 0; return } + let size = rt_image_st.rt_file_len let pal: words = words(128) fill(pal, 0, 128 * 4) var cur = -1 @@ -435,10 +437,10 @@ function rt_sprites_load(path: string) -> void { } if c0 == 's' { # 's' — "spr" starts a new sprite cur = -1 - if spr_n < SPR_MAX { - cur = spr_n - spr_n += 1 - fill(offset(spr_px, cur * SPR_SZ * SPR_SZ * 4), 0, SPR_SZ * SPR_SZ * 4) + if rt_image_st.spr_n < SPR_MAX { + cur = rt_image_st.spr_n + rt_image_st.spr_n += 1 + fill(offset(rt_image_st.spr_px, cur * SPR_SZ * SPR_SZ * 4), 0, SPR_SZ * SPR_SZ * 4) } row = 0 } @@ -449,7 +451,7 @@ function rt_sprites_load(path: string) -> void { if row < SPR_SZ { for x in 0 .. SPR_SZ { if x < len { - spr_px[cur * SPR_SZ * SPR_SZ + row * SPR_SZ + x] = pal[(d[start + x] & 127)] + rt_image_st.spr_px[cur * SPR_SZ * SPR_SZ + row * SPR_SZ + x] = pal[(d[start + x] & 127)] } } row += 1 diff --git a/runtime/native/inflate.ludic b/runtime/native/inflate.ludic index 100136c7..73fc2cb0 100644 --- a/runtime/native/inflate.ludic +++ b/runtime/native/inflate.ludic @@ -16,47 +16,53 @@ # ============================================================================ # ---- bit reader (DEFLATE packs bits least-significant-first) --------------- -var z_src: pointer = 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 +export state RtInflateState { + z_src: pointer = null + z_len: int = 0 + z_pos: int = 0 + z_bitbuf: int = 0 + z_bitcnt: int = 0 + z_err: int = 0 + z_lbase: words = null + z_lext: words = null + z_dbase: words = null + z_dext: words = null +} -function z_start(src: pointer, len: int) -> void { - z_tables_once() - z_src = src - z_len = len - z_pos = 0 - z_bitbuf = 0 - z_bitcnt = 0 - z_err = 0 +function z_start(rt_inflate_st: mut RtInflateState, src: pointer, len: int) -> void { + z_tables_once(rt_inflate_st) + rt_inflate_st.z_src = src + rt_inflate_st.z_len = len + rt_inflate_st.z_pos = 0 + rt_inflate_st.z_bitbuf = 0 + rt_inflate_st.z_bitcnt = 0 + rt_inflate_st.z_err = 0 } # Fill the bit buffer to at least `n` bits without consuming any (n <= 16, so the # buffer never shifts a byte past bit 15 and cannot reach the sign bit). -function z_need(n: int) -> void { - while z_bitcnt < n { - if z_pos >= z_len { return } - z_bitbuf = (z_bitbuf | (z_src[z_pos] << z_bitcnt)) - z_pos += 1 - z_bitcnt += 8 +function z_need(rt_inflate_st: mut RtInflateState, n: int) -> void { + while rt_inflate_st.z_bitcnt < n { + if rt_inflate_st.z_pos >= rt_inflate_st.z_len { return } + rt_inflate_st.z_bitbuf = (rt_inflate_st.z_bitbuf | (rt_inflate_st.z_src[rt_inflate_st.z_pos] << rt_inflate_st.z_bitcnt)) + rt_inflate_st.z_pos += 1 + rt_inflate_st.z_bitcnt += 8 } } -function z_bits(need: int) -> int { - var val = z_bitbuf - while z_bitcnt < need { - if z_pos >= z_len { - z_err = 1 +function z_bits(rt_inflate_st: mut RtInflateState, need: int) -> int { + var val = rt_inflate_st.z_bitbuf + while rt_inflate_st.z_bitcnt < need { + if rt_inflate_st.z_pos >= rt_inflate_st.z_len { + rt_inflate_st.z_err = 1 return 0 } - val = (val | (z_src[z_pos] << z_bitcnt)) - z_pos += 1 - z_bitcnt += 8 + val = (val | (rt_inflate_st.z_src[rt_inflate_st.z_pos] << rt_inflate_st.z_bitcnt)) + rt_inflate_st.z_pos += 1 + rt_inflate_st.z_bitcnt += 8 } - z_bitbuf = (val >> need) - z_bitcnt -= need + rt_inflate_st.z_bitbuf = (val >> need) + rt_inflate_st.z_bitcnt -= need return (val & ((1 << need) - 1)) } @@ -140,14 +146,14 @@ function z_table_build(table: words, lengths: words, n: int) -> void { free(firstc) } -function z_decode(table: words) -> int { - z_need(Z_FAST) - if z_bitcnt >= Z_FAST { - let e = table[16 + (z_bitbuf & (Z_FASTSZ - 1))] +function z_decode(rt_inflate_st: mut RtInflateState, table: words) -> int { + z_need(rt_inflate_st, Z_FAST) + if rt_inflate_st.z_bitcnt >= Z_FAST { + let e = table[16 + (rt_inflate_st.z_bitbuf & (Z_FASTSZ - 1))] if e != 0 { let l = (e >> 16) - z_bitbuf = (z_bitbuf >> l) - z_bitcnt -= l + rt_inflate_st.z_bitbuf = (rt_inflate_st.z_bitbuf >> l) + rt_inflate_st.z_bitcnt -= l return (e & 65535) } } @@ -156,7 +162,7 @@ function z_decode(table: words) -> int { var first = 0 var index = 0 for len in 1 .. 16 { - code = (code | z_bits(1)) + code = (code | z_bits(rt_inflate_st, 1)) let count = table[len] if code - first < count { return table[Z_SYMS + index + (code - first)] @@ -165,7 +171,7 @@ function z_decode(table: words) -> int { first = ((first + count) << 1) code = (code << 1) } - z_err = 1 + rt_inflate_st.z_err = 1 return -1 } @@ -198,51 +204,47 @@ function z_dist_extra(sym: int) -> int { # The RFC tables above are pure functions of the symbol; compute them once rather # than dividing per match. -var z_lbase: words = null -var z_lext: words = null -var z_dbase: words = null -var z_dext: words = null -function z_tables_once() -> void { - if z_lbase != null { return } - z_lbase = words(29) - z_lext = words(29) +function z_tables_once(rt_inflate_st: mut RtInflateState) -> void { + if rt_inflate_st.z_lbase != null { return } + rt_inflate_st.z_lbase = words(29) + rt_inflate_st.z_lext = words(29) for s in 0 .. 29 { - z_lbase[s] = z_len_base(s) - z_lext[s] = z_len_extra(s) + rt_inflate_st.z_lbase[s] = z_len_base(s) + rt_inflate_st.z_lext[s] = z_len_extra(s) } - z_dbase = words(30) - z_dext = words(30) + rt_inflate_st.z_dbase = words(30) + rt_inflate_st.z_dext = words(30) for s in 0 .. 30 { - z_dbase[s] = z_dist_base(s) - z_dext[s] = z_dist_extra(s) + rt_inflate_st.z_dbase[s] = z_dist_base(s) + rt_inflate_st.z_dext[s] = z_dist_extra(s) } } # ---- block decoders ------------------------------------------------------- # `out` is the destination window; returns the new write position, or -1. -function z_stored(out: pointer, 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 = z_src[z_pos] + (z_src[z_pos + 1] << 8) - z_pos += 4 # LEN then its one's complement +function z_stored(rt_inflate_st: mut RtInflateState, out: pointer, at: int, cap: int) -> int { + rt_inflate_st.z_bitbuf = 0 + rt_inflate_st.z_bitcnt = 0 # stored blocks are byte-aligned + if rt_inflate_st.z_pos + 4 > rt_inflate_st.z_len { return -1 } + let n = rt_inflate_st.z_src[rt_inflate_st.z_pos] + (rt_inflate_st.z_src[rt_inflate_st.z_pos + 1] << 8) + rt_inflate_st.z_pos += 4 # LEN then its one's complement var w = at for i in 0 .. n { - if z_pos >= z_len { return -1 } + if rt_inflate_st.z_pos >= rt_inflate_st.z_len { return -1 } if w >= cap { return -1 } - out[w] = z_src[z_pos] + out[w] = rt_inflate_st.z_src[rt_inflate_st.z_pos] w += 1 - z_pos += 1 + rt_inflate_st.z_pos += 1 } return w } -function z_codes(out: pointer, at: int, cap: int, lit: words, dist: words) -> int { +function z_codes(rt_inflate_st: mut RtInflateState, out: pointer, at: int, cap: int, lit: words, dist: words) -> int { var w = at - var sym = z_decode(lit) + var sym = z_decode(rt_inflate_st, lit) while sym != 256 { - if z_err != 0 { return -1 } + if rt_inflate_st.z_err != 0 { return -1 } if sym < 0 { return -1 } if sym < 256 { if w >= cap { return -1 } @@ -252,11 +254,11 @@ function z_codes(out: pointer, at: int, cap: int, lit: words, dist: words) -> in if sym > 256 { let s = sym - 257 if s >= 29 { return -1 } - let length = z_lbase[s] + z_bits(z_lext[s]) - let d = z_decode(dist) + let length = rt_inflate_st.z_lbase[s] + z_bits(rt_inflate_st, rt_inflate_st.z_lext[s]) + let d = z_decode(rt_inflate_st, dist) if d < 0 { return -1 } if d >= 30 { return -1 } - let distance = z_dbase[d] + z_bits(z_dext[d]) + let distance = rt_inflate_st.z_dbase[d] + z_bits(rt_inflate_st, rt_inflate_st.z_dext[d]) if distance > w { return -1 } if w + length > cap { return -1 } # bounds once, not per byte var sp = w - distance @@ -268,7 +270,7 @@ function z_codes(out: pointer, at: int, cap: int, lit: words, dist: words) -> in k += 1 } } - sym = z_decode(lit) + sym = z_decode(rt_inflate_st, lit) } return w } @@ -285,10 +287,10 @@ function z_fixed_tables(lit: words, dist: words) -> void { free(lengths) } -function z_dynamic_tables(lit: words, dist: words) -> int { - let nlen = z_bits(5) + 257 - let ndist = z_bits(5) + 1 - let ncode = z_bits(4) + 4 +function z_dynamic_tables(rt_inflate_st: mut RtInflateState, lit: words, dist: words) -> int { + let nlen = z_bits(rt_inflate_st, 5) + 257 + let ndist = z_bits(rt_inflate_st, 5) + 1 + let ncode = z_bits(rt_inflate_st, 4) + 4 if nlen > 286 { return 0 } if ndist > 30 { return 0 } @@ -298,14 +300,14 @@ function z_dynamic_tables(lit: words, dist: words) -> int { # 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 { - lengths[order[i] - 48] = z_bits(3) + lengths[order[i] - 48] = z_bits(rt_inflate_st, 3) } let clen = z_table_new(19) z_table_build(clen, lengths, 19) var n = 0 while n < nlen + ndist { - let sym = z_decode(clen) + let sym = z_decode(rt_inflate_st, clen) if sym < 0 { return 0 } if sym < 16 { lengths[n] = sym @@ -317,10 +319,10 @@ function z_dynamic_tables(lit: words, dist: words) -> int { if sym == 16 { if n == 0 { return 0 } prev = lengths[n - 1] - rep = 3 + z_bits(2) + rep = 3 + z_bits(rt_inflate_st, 2) } - if sym == 17 { rep = 3 + z_bits(3) } - if sym == 18 { rep = 11 + z_bits(7) } + if sym == 17 { rep = 3 + z_bits(rt_inflate_st, 3) } + if sym == 18 { rep = 11 + z_bits(rt_inflate_st, 7) } for k in 0 .. rep { if n < 320 { lengths[n] = prev @@ -341,24 +343,24 @@ function z_dynamic_tables(lit: words, dist: words) -> int { } # Inflate a raw DEFLATE stream. Returns bytes written, or -1. -function z_inflate(src: pointer, len: int, out: pointer, cap: int) -> int { - z_start(src, len) +function z_inflate(rt_inflate_st: mut RtInflateState, src: pointer, len: int, out: pointer, cap: int) -> int { + z_start(rt_inflate_st, src, len) let lit = z_table_new(288) let dist = z_table_new(30) var w = 0 var 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) } + final = z_bits(rt_inflate_st, 1) + let btype = z_bits(rt_inflate_st, 2) + if rt_inflate_st.z_err != 0 { return -1 } + if btype == 0 { w = z_stored(rt_inflate_st, out, w, cap) } if btype == 1 { z_fixed_tables(lit, dist) - w = z_codes(out, w, cap, lit, dist) + w = z_codes(rt_inflate_st, 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 z_dynamic_tables(rt_inflate_st, lit, dist) == 0 { return -1 } + w = z_codes(rt_inflate_st, out, w, cap, lit, dist) } if btype == 3 { return -1 } if w < 0 { return -1 } @@ -369,11 +371,11 @@ function z_inflate(src: pointer, len: int, out: pointer, cap: int) -> int { } # zlib wrapper (RFC 1950): two header bytes, then DEFLATE, then Adler-32. -function z_uncompress(src: pointer, len: int, out: pointer, cap: int) -> int { +function z_uncompress(rt_inflate_st: mut RtInflateState, src: pointer, len: int, out: pointer, cap: int) -> int { if len < 2 { return -1 } let cmf = src[0] if (cmf & 15) != 8 { return -1 } - return z_inflate(offset(src, 2), len - 2, out, cap) + return z_inflate(rt_inflate_st, offset(src, 2), len - 2, out, cap) } # gzip framing (RFC 1952): a 10-byte header (magic 1f 8b, CM=8, FLG, 4-byte MTIME, @@ -382,7 +384,7 @@ function z_uncompress(src: pointer, len: int, out: pointer, cap: int) -> int { # the header + optional fields, inflate the body, and ignore the trailer — the # CRC is a redundancy check, not needed to decode (PNG likewise ignores ancillary # CRCs). Returns bytes written, or -1. -function z_gunzip(src: pointer, len: int, out: pointer, cap: int) -> int { +function z_gunzip(rt_inflate_st: mut RtInflateState, src: pointer, len: int, out: pointer, cap: int) -> int { if len < 18 { return -1 } # 10 header + 8 trailer minimum if src[0] != 31 { return -1 } # 0x1f if src[1] != 139 { return -1 } # 0x8b @@ -404,5 +406,5 @@ function z_gunzip(src: pointer, len: int, out: pointer, cap: int) -> int { } if (flg & 2) != 0 { pos += 2 } # FHCRC: 2-byte header CRC if pos + 8 > len { return -1 } - return z_inflate(offset(src, pos), len - pos - 8, out, cap) + return z_inflate(rt_inflate_st, offset(src, pos), len - pos - 8, out, cap) } diff --git a/runtime/native/input.ludic b/runtime/native/input.ludic index f6889818..9f072b09 100644 --- a/runtime/native/input.ludic +++ b/runtime/native/input.ludic @@ -24,62 +24,91 @@ const INPUT_MAX_ACT: int = 32 # named actions const INPUT_MAX_KEYS: int = 4 # physical keys bound per action const INPUT_REC_CAP: int = 8192 # recordable frames -var input_names: pointers = null # action name per slot (0..input_nact) -var input_keys: words = null # INPUT_MAX_ACT * INPUT_MAX_KEYS key codes (0 = empty) -var input_pads: words = null # INPUT_MAX_ACT * INPUT_MAX_KEYS pad buttons, stored +1 (0 = empty) — #83 -var input_nact: int = 0 +export state RtInputState { + input_names: pointers = null # action name per slot (0..input_nact) + input_keys: words = null # INPUT_MAX_ACT * INPUT_MAX_KEYS key codes (0 = empty) + input_pads: words = null # INPUT_MAX_ACT * INPUT_MAX_KEYS pad buttons, stored +1 (0 = empty) — #83 + input_nact: int = 0 + input_frame: int = 0 # the key polled this frame + input_last: int = 0 # the key polled last frame (for edges) + input_mode: int = 0 # 0 = live, 1 = record, 2 = replay + input_rec: words = null # recorded key per frame + input_recn: int = 0 # frames recorded + input_pos: int = 0 # replay / record cursor + in_have_frame_driver: bool = false + in_ready: bool = false + in_sim: words = null # simulated held set (Input.press / release) — persists + in_dev: words = null # platform / polled held set — refreshed each poll + in_held: words = null # committed effective set this frame (what reads see) + in_prev: words = null # committed set last frame (for edges) + in_mx: int = 0 # current x/y + in_my: int = 0 + in_mx0: int = 0 # x/y at the previous frame (for the delta) + in_my0: int = 0 + in_mdx: int = 0 # delta this frame + in_mdy: int = 0 + in_rdx: int = 0 # the raw motion the platform reports while captured + in_rdy: int = 0 + in_cursor_mode: int = 0 + in_mouse_rebase: bool = true + in_mbtn: int = 0 # button bitmask (bit 0 left, 1 right, 2 middle) + in_wheel: int = 0 # wheel delta this frame + in_pad_conn: words = null # IN_PADS + in_pad_btn: words = null # IN_PADS + in_pad_btn0: words = null # IN_PADS — pad button mask last frame (edges) — #83 + in_pad_axis: fixeds = null # IN_PADS * IN_AXES + in_touch_on: words = null # IN_TOUCH + in_touch_x: words = null # IN_TOUCH + in_touch_y: words = null # IN_TOUCH + in_tape: words = null # INPUT_REC_CAP * IN_STRIDE + in_text_buf: words = null +} -var input_frame: int = 0 # the key polled this frame -var input_last: int = 0 # the key polled last frame (for edges) -var input_mode: int = 0 # 0 = live, 1 = record, 2 = replay -var input_rec: words = null # recorded key per frame -var input_recn: int = 0 # frames recorded -var input_pos: int = 0 # replay / record cursor -function input_init() -> void { - if input_names == null { - input_names = pointers(INPUT_MAX_ACT) # a pointer (8 bytes) per action slot - input_keys = words(INPUT_MAX_ACT * INPUT_MAX_KEYS) - input_pads = words(INPUT_MAX_ACT * INPUT_MAX_KEYS) # #83 pad buttons per action (+1 encoded) +function input_init(rt_input_st: mut RtInputState) -> void { + if rt_input_st.input_names == null { + rt_input_st.input_names = pointers(INPUT_MAX_ACT) # a pointer (8 bytes) per action slot + rt_input_st.input_keys = words(INPUT_MAX_ACT * INPUT_MAX_KEYS) + rt_input_st.input_pads = words(INPUT_MAX_ACT * INPUT_MAX_KEYS) # #83 pad buttons per action (+1 encoded) } } # slot of the action `name`, or -1. Names compare by byte-string equality. -function input_find(name: pointer) -> int { - input_init() +function input_find(rt_input_st: mut RtInputState, name: pointer) -> int { + input_init(rt_input_st) var i = 0 - while i < input_nact { - if input_names[i] == name { return i } + while i < rt_input_st.input_nact { + if rt_input_st.input_names[i] == name { return i } i += 1 } return -1 } # get-or-create the slot for `name`. -function input_slot(name: pointer) -> int { - let f = input_find(name) +function input_slot(rt_input_st: mut RtInputState, name: pointer) -> int { + let f = input_find(rt_input_st, name) if f >= 0 { return f } - if input_nact >= INPUT_MAX_ACT { return INPUT_MAX_ACT - 1 } # silently reuse the last slot when full - let s = input_nact - input_names[s] = name - input_nact += 1 + if rt_input_st.input_nact >= INPUT_MAX_ACT { return INPUT_MAX_ACT - 1 } # silently reuse the last slot when full + let s = rt_input_st.input_nact + rt_input_st.input_names[s] = name + rt_input_st.input_nact += 1 return s } # bind physical `key` to the named action, creating the action if new. A key # already bound to the action is left as-is (idempotent). -function input_bind(name: pointer, key: int) -> void { - let s = input_slot(name) +function input_bind(rt_input_st: mut RtInputState, name: pointer, key: int) -> void { + let s = input_slot(rt_input_st, name) let base = s * INPUT_MAX_KEYS var i = 0 while i < INPUT_MAX_KEYS { - if input_keys[base + i] == key { return } # already bound + if rt_input_st.input_keys[base + i] == key { return } # already bound i += 1 } i = 0 while i < INPUT_MAX_KEYS { - if input_keys[base + i] == 0 { input_keys[base + i] = key; return } + if rt_input_st.input_keys[base + i] == 0 { rt_input_st.input_keys[base + i] = key; return } i += 1 } } @@ -87,56 +116,56 @@ function input_bind(name: pointer, key: int) -> void { # #83 — a *default* binding: bind `key` only if the action has no key bound yet. # A game ships its defaults with Input.action in Boot; a player's later Input.rebind # (or a loaded key-map) is not clobbered, and re-running the defaults is idempotent. -function input_default(name: pointer, key: int) -> void { - let s = input_slot(name) +function input_default(rt_input_st: mut RtInputState, name: pointer, key: int) -> void { + let s = input_slot(rt_input_st, name) let base = s * INPUT_MAX_KEYS var i = 0 while i < INPUT_MAX_KEYS { - if input_keys[base + i] != 0 { return } # already has a binding — keep it + if rt_input_st.input_keys[base + i] != 0 { return } # already has a binding — keep it i += 1 } - input_keys[base] = key + rt_input_st.input_keys[base] = key } # #83 — device-agnostic actions: also fire the named action from a gamepad button. # Buttons are stored +1 so 0 stays the empty marker. The same action can carry both # keyboard keys (input_bind / input_default) and pad buttons; a read fires on either. -function input_bind_pad(name: pointer, button: int) -> void { - let s = input_slot(name) +function input_bind_pad(rt_input_st: mut RtInputState, name: pointer, button: int) -> void { + let s = input_slot(rt_input_st, name) let base = s * INPUT_MAX_KEYS var i = 0 while i < INPUT_MAX_KEYS { - if input_pads[base + i] == (button + 1) { return } # already bound + if rt_input_st.input_pads[base + i] == (button + 1) { return } # already bound i += 1 } i = 0 while i < INPUT_MAX_KEYS { - if input_pads[base + i] == 0 { input_pads[base + i] = button + 1; return } + if rt_input_st.input_pads[base + i] == 0 { rt_input_st.input_pads[base + i] = button + 1; return } i += 1 } } # runtime rebinding: replace `oldkey` with `newkey` on the named action. A no-op # if the action or the old key is not found. -function input_rebind(name: pointer, oldkey: int, newkey: int) -> void { - let s = input_find(name) +function input_rebind(rt_input_st: mut RtInputState, name: pointer, oldkey: int, newkey: int) -> void { + let s = input_find(rt_input_st, name) if s < 0 { return } let base = s * INPUT_MAX_KEYS var i = 0 while i < INPUT_MAX_KEYS { - if input_keys[base + i] == oldkey { input_keys[base + i] = newkey; return } + if rt_input_st.input_keys[base + i] == oldkey { rt_input_st.input_keys[base + i] = newkey; return } i += 1 } } # does key `k` (0 = none) fire the action in slot `s`? -function input_slot_has(s: int, k: int) -> bool { +function input_slot_has(rt_input_st: RtInputState, s: int, k: int) -> bool { if s < 0 { return false } if k == 0 { return false } let base = s * INPUT_MAX_KEYS var i = 0 while i < INPUT_MAX_KEYS { - if input_keys[base + i] == k { return true } + if rt_input_st.input_keys[base + i] == k { return true } i += 1 } return false @@ -149,44 +178,43 @@ function input_slot_has(s: int, k: int) -> bool { # commit copied in_held into in_prev twice, which destroyed the key_pressed / # key_released edges (in_prev ended up equal to in_held). An entry-driven harness # has no loop, so the flag stays false and each Input.poll commits a frame as before. -var in_have_frame_driver: bool = false # The actual per-frame input read: read the live key (or a recorded one), advance # the record/replay tape, and rebuild the multi-key device layer (held keys, mouse, # gamepad — #50). Returns the frame's key. -function input_commit() -> int { - input_last = input_frame - if input_mode == 2 { # replay +function input_commit(rt_core_st: mut RtCoreState, rt_input_st: mut RtInputState) -> int { + rt_input_st.input_last = rt_input_st.input_frame + if rt_input_st.input_mode == 2 { # replay var k = 0 - if input_pos < input_recn { k = input_rec[input_pos]; input_pos += 1 } - input_frame = k - input_device_commit(k, 1) # rebuild the device state from the tape + if rt_input_st.input_pos < rt_input_st.input_recn { k = rt_input_st.input_rec[rt_input_st.input_pos]; rt_input_st.input_pos += 1 } + rt_input_st.input_frame = k + input_device_commit(rt_input_st, k, 1) # rebuild the device state from the tape return k } - let k = rt_poll() - if input_mode == 1 { # record - if input_rec == null { input_rec = words(INPUT_REC_CAP) } - if input_recn < INPUT_REC_CAP { input_rec[input_recn] = k; input_recn += 1 } + let k = rt_poll(rt_core_st) + if rt_input_st.input_mode == 1 { # record + if rt_input_st.input_rec == null { rt_input_st.input_rec = words(INPUT_REC_CAP) } + if rt_input_st.input_recn < INPUT_REC_CAP { rt_input_st.input_rec[rt_input_st.input_recn] = k; rt_input_st.input_recn += 1 } } - input_frame = k - input_device_commit(k, 0) + rt_input_st.input_frame = k + input_device_commit(rt_input_st, k, 0) return k } # Called by the generated frame loop once per frame (#83). Marks that a loop is # driving input so a later manual Input.poll this frame does not double-commit. -function input_drive() -> int { - in_have_frame_driver = true - return input_commit() +function input_drive(rt_core_st: mut RtCoreState, rt_input_st: mut RtInputState) -> int { + rt_input_st.in_have_frame_driver = true + return input_commit(rt_core_st, rt_input_st) } # Input.poll — the single per-frame input read a game can call by hand. In a # frame-loop game the loop already drove input this frame (input_drive), so this is # a no-op that returns the frame's key; in an entry-driven harness (no loop) it # commits a frame of input each call, exactly as before. -function input_poll() -> int { - if in_have_frame_driver { return input_frame } - return input_commit() +function input_poll(rt_core_st: mut RtCoreState, rt_input_st: mut RtInputState) -> int { + if rt_input_st.in_have_frame_driver { return rt_input_st.input_frame } + return input_commit(rt_core_st, rt_input_st) } # ============================================================================ @@ -211,53 +239,28 @@ const IN_AXES: int = 4 # axes per pad (2 sticks: lx, ly, rx, ry) const IN_TOUCH: int = 8 # simultaneous touch points const IN_STRIDE: int = 12 # tape words / frame: 8 held + mx, my, buttons, wheel -var in_ready: bool = false -var in_sim: words = null # simulated held set (Input.press / release) — persists -var in_dev: words = null # platform / polled held set — refreshed each poll -var in_held: words = null # committed effective set this frame (what reads see) -var in_prev: words = null # committed set last frame (for edges) # mouse -var in_mx: int = 0 # current x/y -var in_my: int = 0 -var in_mx0: int = 0 # x/y at the previous frame (for the delta) -var in_my0: int = 0 -var in_mdx: int = 0 # delta this frame -var in_mdy: int = 0 -var in_rdx: int = 0 # the raw motion the platform reports while captured -var in_rdy: int = 0 -var in_cursor_mode: int = 0 # the next commit reports no mouse delta: true before the first position is read (the previous one # is not a position, it is 0,0) and after a cursor-mode change (the position source switches between # the virtual reticle and the real cursor, which are unrelated points) -var in_mouse_rebase: bool = true -var in_mbtn: int = 0 # button bitmask (bit 0 left, 1 right, 2 middle) -var in_wheel: int = 0 # wheel delta this frame # gamepads: connected flag, button bitmask, and IN_AXES fixed axes each -var in_pad_conn: words = null # IN_PADS -var in_pad_btn: words = null # IN_PADS -var in_pad_btn0: words = null # IN_PADS — pad button mask last frame (edges) — #83 -var in_pad_axis: fixeds = null # IN_PADS * IN_AXES # touch points: active flag, x, y each -var in_touch_on: words = null # IN_TOUCH -var in_touch_x: words = null # IN_TOUCH -var in_touch_y: words = null # IN_TOUCH # full-state tape (held + mouse), recorded / replayed alongside the key tape -var in_tape: words = null # INPUT_REC_CAP * IN_STRIDE -function in_init() -> void { - if in_ready { return } - in_sim = words(IN_WORDS) - in_dev = words(IN_WORDS) - in_held = words(IN_WORDS) - in_prev = words(IN_WORDS) - in_pad_conn = words(IN_PADS) - in_pad_btn = words(IN_PADS) - in_pad_btn0 = words(IN_PADS) - in_pad_axis = fixeds(IN_PADS * IN_AXES) - in_touch_on = words(IN_TOUCH) - in_touch_x = words(IN_TOUCH) - in_touch_y = words(IN_TOUCH) - in_ready = true +function in_init(rt_input_st: mut RtInputState) -> void { + if rt_input_st.in_ready { return } + rt_input_st.in_sim = words(IN_WORDS) + rt_input_st.in_dev = words(IN_WORDS) + rt_input_st.in_held = words(IN_WORDS) + rt_input_st.in_prev = words(IN_WORDS) + rt_input_st.in_pad_conn = words(IN_PADS) + rt_input_st.in_pad_btn = words(IN_PADS) + rt_input_st.in_pad_btn0 = words(IN_PADS) + rt_input_st.in_pad_axis = fixeds(IN_PADS * IN_AXES) + rt_input_st.in_touch_on = words(IN_TOUCH) + rt_input_st.in_touch_x = words(IN_TOUCH) + rt_input_st.in_touch_y = words(IN_TOUCH) + rt_input_st.in_ready = true } # ---- key-set bit helpers --------------------------------------------------- @@ -278,38 +281,38 @@ function in_set_or(dst: words, a: words, b: words) -> void { var i = 0; while i # ---- the per-frame device commit (called by input_poll) -------------------- # Snapshot the committed set into prev (for edges), refresh the platform set (or # rebuild it from the tape on replay), then recombine into the committed set. -function input_device_commit(k: int, replaying: int) -> void { - in_init() - in_set_copy(in_prev, in_held) # last frame's committed set +function input_device_commit(rt_input_st: mut RtInputState, k: int, replaying: int) -> void { + in_init(rt_input_st) + in_set_copy(rt_input_st.in_prev, rt_input_st.in_held) # last frame's committed set # #83: snapshot last frame's pad-button masks for the just_pressed/released edges. # Taken before the platform refresh (win_pad, below, runs after this), so it holds # the previous frame's committed value against which this frame's edge is measured. var pj = 0 - while pj < IN_PADS { in_pad_btn0[pj] = in_pad_btn[pj]; pj += 1 } + while pj < IN_PADS { rt_input_st.in_pad_btn0[pj] = rt_input_st.in_pad_btn[pj]; pj += 1 } if replaying == 1 { # rebuild the platform set + mouse from the tape; sim/injection is ignored so # a replay is authoritative (as #7's key replay ignores the live device). - let base = (input_pos - 1) * IN_STRIDE - if (in_tape != null) and (base >= 0) { + let base = (rt_input_st.input_pos - 1) * IN_STRIDE + if (rt_input_st.in_tape != null) and (base >= 0) { var i = 0 - while i < IN_WORDS { in_dev[i] = in_tape[base + i]; i += 1 } - in_mx = in_tape[base + 8] - in_my = in_tape[base + 9] - in_mbtn = in_tape[base + 10] - in_wheel = in_tape[base + 11] + while i < IN_WORDS { rt_input_st.in_dev[i] = rt_input_st.in_tape[base + i]; i += 1 } + rt_input_st.in_mx = rt_input_st.in_tape[base + 8] + rt_input_st.in_my = rt_input_st.in_tape[base + 9] + rt_input_st.in_mbtn = rt_input_st.in_tape[base + 10] + rt_input_st.in_wheel = rt_input_st.in_tape[base + 11] } - in_set_copy(in_held, in_dev) + in_set_copy(rt_input_st.in_held, rt_input_st.in_dev) } else { # live: fill the platform set from the window (real simultaneous keys) or, # headless, from the single polled key. Injection (in_sim) is OR-ed on top. if is_windowed() { - win_held(in_dev) + win_held(rt_input_st.in_dev) let mbuf = words(6) # [x, y, button-mask, wheel, raw dx, raw dy] mbuf[4] = 0; mbuf[5] = 0 win_mouse(mbuf) - in_mx = mbuf[0]; in_my = mbuf[1]; in_mbtn = mbuf[2]; in_wheel = mbuf[3] - in_rdx = mbuf[4]; in_rdy = mbuf[5] + rt_input_st.in_mx = mbuf[0]; rt_input_st.in_my = mbuf[1]; rt_input_st.in_mbtn = mbuf[2]; rt_input_st.in_wheel = mbuf[3] + rt_input_st.in_rdx = mbuf[4]; rt_input_st.in_rdy = mbuf[5] # #51 — feed the platform gamepad + touch state into the same buffers the # read APIs use. Each is windowed-only glue (win_pad / win_touch are DCE'd # in a headless build); on hardware they overwrite the injected state. @@ -318,7 +321,7 @@ function input_device_commit(k: int, replaying: int) -> void { var pi = 0 while pi < IN_PADS { let pb = pi * 6 - input_set_pad(pi, pbuf[pb] != 0, pbuf[pb + 1], + input_set_pad(rt_input_st, pi, pbuf[pb] != 0, pbuf[pb + 1], as_fixed(pbuf[pb + 2]), as_fixed(pbuf[pb + 3]), as_fixed(pbuf[pb + 4]), as_fixed(pbuf[pb + 5])) pi += 1 @@ -328,49 +331,49 @@ function input_device_commit(k: int, replaying: int) -> void { var ti = 0 while ti < IN_TOUCH { let tb = ti * 3 - input_set_touch(ti, tbuf[tb + 1], tbuf[tb + 2], tbuf[tb] != 0) + input_set_touch(rt_input_st, ti, tbuf[tb + 1], tbuf[tb + 2], tbuf[tb] != 0) ti += 1 } } else { - in_set_clear(in_dev) - if k > 0 { in_bit_set(in_dev, k, true) } + in_set_clear(rt_input_st.in_dev) + if k > 0 { in_bit_set(rt_input_st.in_dev, k, true) } } - in_set_or(in_held, in_dev, in_sim) - if input_mode == 1 { input_device_record() } # snapshot the frame into the tape + in_set_or(rt_input_st.in_held, rt_input_st.in_dev, rt_input_st.in_sim) + if rt_input_st.input_mode == 1 { input_device_record(rt_input_st) } # snapshot the frame into the tape } # mouse delta vs the previous frame's committed position (in_mx set by the # platform above when windowed, by Input.set_mouse before this poll otherwise). - in_mdx = in_mx - in_mx0 - in_mdy = in_my - in_my0 + rt_input_st.in_mdx = rt_input_st.in_mx - rt_input_st.in_mx0 + rt_input_st.in_mdy = rt_input_st.in_my - rt_input_st.in_my0 # captured (mode 2): the cursor is a clamped reticle, the motion is the raw delta - if is_windowed() and in_cursor_mode == 2 { in_mdx = in_rdx; in_mdy = in_rdy } + if is_windowed() and rt_input_st.in_cursor_mode == 2 { rt_input_st.in_mdx = rt_input_st.in_rdx; rt_input_st.in_mdy = rt_input_st.in_rdy } # No motion on the first frame or across a cursor-mode change. The previous position there is # 0,0 or a point in another coordinate source, so the difference is the cursor's whole distance # from it: a camera that adds mouse_dy to its pitch came up pointing at the ground. - if in_mouse_rebase { in_mdx = 0; in_mdy = 0; in_mouse_rebase = false } - in_mx0 = in_mx - in_my0 = in_my + if rt_input_st.in_mouse_rebase { rt_input_st.in_mdx = 0; rt_input_st.in_mdy = 0; rt_input_st.in_mouse_rebase = false } + rt_input_st.in_mx0 = rt_input_st.in_mx + rt_input_st.in_my0 = rt_input_st.in_my } # write this frame's committed set + mouse into the tape at the record cursor. -function input_device_record() -> void { - if in_tape == null { in_tape = words(INPUT_REC_CAP * IN_STRIDE) } - let f = input_recn - 1 +function input_device_record(rt_input_st: mut RtInputState) -> void { + if rt_input_st.in_tape == null { rt_input_st.in_tape = words(INPUT_REC_CAP * IN_STRIDE) } + let f = rt_input_st.input_recn - 1 if (f < 0) or (f >= INPUT_REC_CAP) { return } let base = f * IN_STRIDE var i = 0 - while i < IN_WORDS { in_tape[base + i] = in_held[i]; i += 1 } - in_tape[base + 8] = in_mx - in_tape[base + 9] = in_my - in_tape[base + 10] = in_mbtn - in_tape[base + 11] = in_wheel + while i < IN_WORDS { rt_input_st.in_tape[base + i] = rt_input_st.in_held[i]; i += 1 } + rt_input_st.in_tape[base + 8] = rt_input_st.in_mx + rt_input_st.in_tape[base + 9] = rt_input_st.in_my + rt_input_st.in_tape[base + 10] = rt_input_st.in_mbtn + rt_input_st.in_tape[base + 11] = rt_input_st.in_wheel } # ---- held keys ------------------------------------------------------------- -function input_key_down(k: int) -> bool { in_init(); return in_bit_get(in_held, k) } -function input_key_pressed(k: int) -> bool { in_init(); return in_bit_get(in_held, k) and (not in_bit_get(in_prev, k)) } -function input_key_released(k: int) -> bool { in_init(); return (not in_bit_get(in_held, k)) and in_bit_get(in_prev, k) } +function input_key_down(rt_input_st: mut RtInputState, k: int) -> bool { in_init(rt_input_st); return in_bit_get(rt_input_st.in_held, k) } +function input_key_pressed(rt_input_st: mut RtInputState, k: int) -> bool { in_init(rt_input_st); return in_bit_get(rt_input_st.in_held, k) and (not in_bit_get(rt_input_st.in_prev, k)) } +function input_key_released(rt_input_st: mut RtInputState, k: int) -> bool { in_init(rt_input_st); return (not in_bit_get(rt_input_st.in_held, k)) and in_bit_get(rt_input_st.in_prev, k) } # The name to show a player for key code `k`. A letter, digit or punctuation code is a # PHYSICAL key - the one that types it on a US layout - so the name is what the player's @@ -421,19 +424,18 @@ function input_key_label(k: int) -> string { # actually produce. Building text out of key codes instead meant a Turkish player could not type # c-cedilla, g-breve, dotless i, o-umlaut, s-cedilla or u-umlaut anywhere - their own name included. # Empty headless, and where the platform has no text channel. -var in_text_buf: words = null -function input_text() -> string { +function input_text(rt_input_st: mut RtInputState) -> string { if not is_windowed() { return "" } - if in_text_buf == null { in_text_buf = words(64) } - let n = win_text(in_text_buf, 64) + if rt_input_st.in_text_buf == null { rt_input_st.in_text_buf = words(64) } + let n = win_text(rt_input_st.in_text_buf, 64) if n <= 0 { return "" } var out = "" var i = 0 while i < n { - var c = in_text_buf[i] + var c = rt_input_st.in_text_buf[i] # a code point outside the BMP arrives as a surrogate PAIR - two units, one character if c >= 55296 and c < 56320 and i + 1 < n { - let lo = in_text_buf[i + 1] + let lo = rt_input_st.in_text_buf[i + 1] if lo >= 56320 and lo < 57344 { c = 65536 + ((c - 55296) * 1024) + (lo - 56320); i = i + 1 } } out = out + in_utf8(c) @@ -455,27 +457,27 @@ function in_utf8(c: int) -> string { } # inject a held key (AI, tutorial, testing, network) — persists until released. -function input_press(k: int) -> void { in_init(); in_bit_set(in_sim, k, true) } -function input_release(k: int) -> void { in_init(); in_bit_set(in_sim, k, false) } +function input_press(rt_input_st: mut RtInputState, k: int) -> void { in_init(rt_input_st); in_bit_set(rt_input_st.in_sim, k, true) } +function input_release(rt_input_st: mut RtInputState, k: int) -> void { in_init(rt_input_st); in_bit_set(rt_input_st.in_sim, k, false) } # ---- analog from keys ------------------------------------------------------ # A digital axis: +1.0 if the positive key is held, -1.0 if the negative, else 0. -function input_axis(neg: int, pos: int) -> fixed { - in_init() +function input_axis(rt_input_st: mut RtInputState, neg: int, pos: int) -> fixed { + in_init(rt_input_st) var v = fixed(0) - if in_bit_get(in_held, pos) { v += fixed(1) } - if in_bit_get(in_held, neg) { v -= fixed(1) } + if in_bit_get(rt_input_st.in_held, pos) { v += fixed(1) } + if in_bit_get(rt_input_st.in_held, neg) { v -= fixed(1) } return v } # #79 — a directional intent as a plain int: +1 if the positive key is held, -1 if # the negative, 0 if neither or both. Reads the multi-key device set, so it needs # no bool->int glue (the `dx = ki(key_down('d')) - ki(key_down('a'))` boilerplate) # and feeds an int mover (TopDown.move) straight: dx = Input.axis_i('a','d'). -function input_axis_i(neg: int, pos: int) -> int { - in_init() +function input_axis_i(rt_input_st: mut RtInputState, neg: int, pos: int) -> int { + in_init(rt_input_st) var v = 0 - if in_bit_get(in_held, pos) { v += 1 } - if in_bit_get(in_held, neg) { v -= 1 } + if in_bit_get(rt_input_st.in_held, pos) { v += 1 } + if in_bit_get(rt_input_st.in_held, neg) { v -= 1 } return v } # a stick has to leave its centre by this much before it counts as a direction @@ -486,13 +488,13 @@ const STICK_LEFT_Y: int = 1 # The standard top-down movement intent as -1/0/1 per axis: WASD or the arrow # keys, and the left stick of pad 0 (past the deadzone) when one is connected. -function input_move_i() -> IVec2 { - in_init() - var x = input_axis_i(Key.A, Key.D) + input_axis_i(Key.Left, Key.Right) - var y = input_axis_i(Key.W, Key.S) + input_axis_i(Key.Up, Key.Down) - if input_pad_connected(0) { - let sx = input_pad_axis(0, STICK_LEFT_X) - let sy = input_pad_axis(0, STICK_LEFT_Y) +function input_move_i(rt_input_st: mut RtInputState) -> IVec2 { + in_init(rt_input_st) + var x = input_axis_i(rt_input_st, Key.A, Key.D) + input_axis_i(rt_input_st, Key.Left, Key.Right) + var y = input_axis_i(rt_input_st, Key.W, Key.S) + input_axis_i(rt_input_st, Key.Up, Key.Down) + if input_pad_connected(rt_input_st, 0) { + let sx = input_pad_axis(rt_input_st, 0, STICK_LEFT_X) + let sy = input_pad_axis(rt_input_st, 0, STICK_LEFT_Y) if sx > STICK_DEADZONE { x = 1 } if sx < -STICK_DEADZONE { x = -1 } if sy > STICK_DEADZONE { y = 1 } @@ -501,14 +503,14 @@ function input_move_i() -> IVec2 { return IVec2.make(clamp(x, -1, 1), clamp(y, -1, 1)) } # A 2D vector from four direction keys, normalized so a diagonal is not faster. -function input_vector(left: int, right: int, up: int, down: int) -> Vector { - in_init() +function input_vector(rt_input_st: mut RtInputState, left: int, right: int, up: int, down: int) -> Vector { + in_init(rt_input_st) var x = fixed(0) var y = fixed(0) - if in_bit_get(in_held, right) { x += fixed(1) } - if in_bit_get(in_held, left) { x -= fixed(1) } - if in_bit_get(in_held, down) { y += fixed(1) } - if in_bit_get(in_held, up) { y -= fixed(1) } + if in_bit_get(rt_input_st.in_held, right) { x += fixed(1) } + if in_bit_get(rt_input_st.in_held, left) { x -= fixed(1) } + if in_bit_get(rt_input_st.in_held, down) { y += fixed(1) } + if in_bit_get(rt_input_st.in_held, up) { y -= fixed(1) } if (x != 0) and (y != 0) { # diagonal: scale by 1/sqrt(2) x *= 0.7071 # fixed multiply (64-bit intermediate) y *= 0.7071 @@ -516,8 +518,8 @@ function input_vector(left: int, right: int, up: int, down: int) -> Vector { return Vector.make(x, y) } # 0.0..1.0 strength of a named action (digital for keys: 1 if any bound key held). -function input_strength(name: pointer) -> fixed { - if input_down(name) { return fixed(1) } +function input_strength(rt_input_st: mut RtInputState, name: pointer) -> fixed { + if input_down(rt_input_st, name) { return fixed(1) } return fixed(0) } @@ -534,82 +536,82 @@ function input_strength(name: pointer) -> fixed { enum CursorMode { Normal, Hidden, Locked, Confined } # Input.cursor_mode(mode:) enum PadButton { A, B, X, Y, LeftShoulder, RightShoulder, Back, Start } # Input.bind_pad(button:) / pad_button enum MouseButton { Left, Right, Middle } # Input.mouse_down(button:) -function input_cursor_mode(mode: int) -> void { - if mode != in_cursor_mode { in_mouse_rebase = true } - in_cursor_mode = mode +function input_cursor_mode(rt_input_st: mut RtInputState, mode: int) -> void { + if mode != rt_input_st.in_cursor_mode { rt_input_st.in_mouse_rebase = true } + rt_input_st.in_cursor_mode = mode if is_windowed() { win_cursor_mode(mode) } } # ---- mouse ----------------------------------------------------------------- -function input_mouse_x() -> int { in_init(); return in_mx } -function input_mouse_y() -> int { in_init(); return in_my } -function input_mouse_dx() -> int { in_init(); return in_mdx } -function input_mouse_dy() -> int { in_init(); return in_mdy } -function input_mouse_down(btn: int) -> bool { in_init(); return (in_mbtn & (1 << btn)) != 0 } -function input_wheel() -> int { in_init(); return in_wheel } +function input_mouse_x(rt_input_st: mut RtInputState) -> int { in_init(rt_input_st); return rt_input_st.in_mx } +function input_mouse_y(rt_input_st: mut RtInputState) -> int { in_init(rt_input_st); return rt_input_st.in_my } +function input_mouse_dx(rt_input_st: mut RtInputState) -> int { in_init(rt_input_st); return rt_input_st.in_mdx } +function input_mouse_dy(rt_input_st: mut RtInputState) -> int { in_init(rt_input_st); return rt_input_st.in_mdy } +function input_mouse_down(rt_input_st: mut RtInputState, btn: int) -> bool { in_init(rt_input_st); return (rt_input_st.in_mbtn & (1 << btn)) != 0 } +function input_wheel(rt_input_st: mut RtInputState) -> int { in_init(rt_input_st); return rt_input_st.in_wheel } # inject the mouse (headless / AI / testing). buttons is a bitmask; wheel is this # frame's delta. -function input_set_mouse(x: int, y: int, buttons: int, wheel: int) -> void { - in_init() - in_mx = x; in_my = y; in_mbtn = buttons; in_wheel = wheel +function input_set_mouse(rt_input_st: mut RtInputState, x: int, y: int, buttons: int, wheel: int) -> void { + in_init(rt_input_st) + rt_input_st.in_mx = x; rt_input_st.in_my = y; rt_input_st.in_mbtn = buttons; rt_input_st.in_wheel = wheel } # ---- gamepads -------------------------------------------------------------- -function input_pad_connected(pad: int) -> bool { - in_init() +function input_pad_connected(rt_input_st: mut RtInputState, pad: int) -> bool { + in_init(rt_input_st) if (pad < 0) or (pad >= IN_PADS) { return false } - return in_pad_conn[pad] != 0 + return rt_input_st.in_pad_conn[pad] != 0 } -function input_pad_button(pad: int, btn: int) -> bool { - in_init() +function input_pad_button(rt_input_st: mut RtInputState, pad: int, btn: int) -> bool { + in_init(rt_input_st) if (pad < 0) or (pad >= IN_PADS) { return false } - return (in_pad_btn[pad] & (1 << btn)) != 0 + return (rt_input_st.in_pad_btn[pad] & (1 << btn)) != 0 } -function input_pad_axis(pad: int, axis: int) -> fixed { - in_init() +function input_pad_axis(rt_input_st: mut RtInputState, pad: int, axis: int) -> fixed { + in_init(rt_input_st) if (pad < 0) or (pad >= IN_PADS) { return fixed(0) } if (axis < 0) or (axis >= IN_AXES) { return fixed(0) } - return in_pad_axis[pad * IN_AXES + axis] + return rt_input_st.in_pad_axis[pad * IN_AXES + axis] } # inject a gamepad's whole state: connected, button bitmask, and four fixed axes. -function input_set_pad(pad: int, connected: bool, buttons: int, lx: fixed, ly: fixed, rx: fixed, ry: fixed) -> void { - in_init() +function input_set_pad(rt_input_st: mut RtInputState, pad: int, connected: bool, buttons: int, lx: fixed, ly: fixed, rx: fixed, ry: fixed) -> void { + in_init(rt_input_st) if (pad < 0) or (pad >= IN_PADS) { return } var c = 0 if connected { c = 1 } - in_pad_conn[pad] = c - in_pad_btn[pad] = buttons + rt_input_st.in_pad_conn[pad] = c + rt_input_st.in_pad_btn[pad] = buttons let b = pad * IN_AXES - in_pad_axis[b] = lx - in_pad_axis[b + 1] = ly - in_pad_axis[b + 2] = rx - in_pad_axis[b + 3] = ry + rt_input_st.in_pad_axis[b] = lx + rt_input_st.in_pad_axis[b + 1] = ly + rt_input_st.in_pad_axis[b + 2] = rx + rt_input_st.in_pad_axis[b + 3] = ry } # ---- touch ----------------------------------------------------------------- -function input_touch_count() -> int { - in_init() +function input_touch_count(rt_input_st: mut RtInputState) -> int { + in_init(rt_input_st) var n = 0 var i = 0 - while i < IN_TOUCH { if in_touch_on[i] != 0 { n += 1 }; i += 1 } + while i < IN_TOUCH { if rt_input_st.in_touch_on[i] != 0 { n += 1 }; i += 1 } return n } -function input_touch_x(i: int) -> int { in_init(); if (i < 0) or (i >= IN_TOUCH) { return 0 }; return in_touch_x[i] } -function input_touch_y(i: int) -> int { in_init(); if (i < 0) or (i >= IN_TOUCH) { return 0 }; return in_touch_y[i] } +function input_touch_x(rt_input_st: mut RtInputState, i: int) -> int { in_init(rt_input_st); if (i < 0) or (i >= IN_TOUCH) { return 0 }; return rt_input_st.in_touch_x[i] } +function input_touch_y(rt_input_st: mut RtInputState, i: int) -> int { in_init(rt_input_st); if (i < 0) or (i >= IN_TOUCH) { return 0 }; return rt_input_st.in_touch_y[i] } # inject a touch point i: active with a position, or inactive. -function input_set_touch(i: int, x: int, y: int, active: bool) -> void { - in_init() +function input_set_touch(rt_input_st: mut RtInputState, i: int, x: int, y: int, active: bool) -> void { + in_init(rt_input_st) if (i < 0) or (i >= IN_TOUCH) { return } var a = 0 if active { a = 1 } - in_touch_on[i] = a - in_touch_x[i] = x - in_touch_y[i] = y + rt_input_st.in_touch_on[i] = a + rt_input_st.in_touch_x[i] = x + rt_input_st.in_touch_y[i] = y } # is the named action held on the frame last polled? -function input_down(name: pointer) -> bool { - return input_slot_has(input_find(name), input_frame) +function input_down(rt_input_st: mut RtInputState, name: pointer) -> bool { + return input_slot_has(rt_input_st, input_find(rt_input_st, name), rt_input_st.input_frame) } # #83 — the Input-Manager reads: an action is *active* when any of its bound @@ -618,55 +620,55 @@ function input_down(name: pointer) -> bool { # see the whole device layer (hold left AND jump), and are device-agnostic. The # frame loop now commits the device layer automatically (input_poll), so these read # live without the game calling Input.poll by hand. -function input_active_in(name: pointer, held: words, padmask: int) -> bool { - let s = input_find(name) +function input_active_in(rt_input_st: mut RtInputState, name: pointer, held: words, padmask: int) -> bool { + let s = input_find(rt_input_st, name) if s < 0 { return false } let base = s * INPUT_MAX_KEYS var i = 0 while i < INPUT_MAX_KEYS { - let k = input_keys[base + i] + let k = rt_input_st.input_keys[base + i] if (k != 0) and in_bit_get(held, k) { return true } - let pb = input_pads[base + i] + let pb = rt_input_st.input_pads[base + i] if (pb != 0) and ((padmask & (1 << (pb - 1))) != 0) { return true } i += 1 } return false } -function input_active(name: pointer) -> bool { - in_init() - return input_active_in(name, in_held, in_pad_btn[0]) +function input_active(rt_input_st: mut RtInputState, name: pointer) -> bool { + in_init(rt_input_st) + return input_active_in(rt_input_st, name, rt_input_st.in_held, rt_input_st.in_pad_btn[0]) } # went active this frame (active now, not last frame) — the deterministic on-press. -function input_just_pressed(name: pointer) -> bool { - in_init() - let now = input_active_in(name, in_held, in_pad_btn[0]) - let was = input_active_in(name, in_prev, in_pad_btn0[0]) +function input_just_pressed(rt_input_st: mut RtInputState, name: pointer) -> bool { + in_init(rt_input_st) + let now = input_active_in(rt_input_st, name, rt_input_st.in_held, rt_input_st.in_pad_btn[0]) + let was = input_active_in(rt_input_st, name, rt_input_st.in_prev, rt_input_st.in_pad_btn0[0]) return now and (not was) } # went inactive this frame (not active now, was last frame) — the on-release. -function input_just_released(name: pointer) -> bool { - in_init() - let now = input_active_in(name, in_held, in_pad_btn[0]) - let was = input_active_in(name, in_prev, in_pad_btn0[0]) +function input_just_released(rt_input_st: mut RtInputState, name: pointer) -> bool { + in_init(rt_input_st) + let now = input_active_in(rt_input_st, name, rt_input_st.in_held, rt_input_st.in_pad_btn[0]) + let was = input_active_in(rt_input_st, name, rt_input_st.in_prev, rt_input_st.in_pad_btn0[0]) return (not now) and was } # did the named action go down this frame (down now, not down last frame)? -function input_pressed(name: pointer) -> bool { - let s = input_find(name) - return input_slot_has(s, input_frame) and (not input_slot_has(s, input_last)) +function input_pressed(rt_input_st: mut RtInputState, name: pointer) -> bool { + let s = input_find(rt_input_st, name) + return input_slot_has(rt_input_st, s, rt_input_st.input_frame) and (not input_slot_has(rt_input_st, s, rt_input_st.input_last)) } # begin recording polled input from the next frame (resets the tape). -function input_record() -> void { - if input_rec == null { input_rec = words(INPUT_REC_CAP) } - input_recn = 0 - input_pos = 0 - input_mode = 1 +function input_record(rt_input_st: mut RtInputState) -> void { + if rt_input_st.input_rec == null { rt_input_st.input_rec = words(INPUT_REC_CAP) } + rt_input_st.input_recn = 0 + rt_input_st.input_pos = 0 + rt_input_st.input_mode = 1 } # replay the recording from its start; subsequent Input.poll calls read the tape. -function input_replay() -> void { - input_pos = 0 - input_mode = 2 +function input_replay(rt_input_st: mut RtInputState) -> void { + rt_input_st.input_pos = 0 + rt_input_st.input_mode = 2 } diff --git a/runtime/native/jobs.ludic b/runtime/native/jobs.ludic index e5c024dc..3403602d 100644 --- a/runtime/native/jobs.ludic +++ b/runtime/native/jobs.ludic @@ -49,45 +49,57 @@ const JK_PRIMES: int = 3 # compute: how many primes are <= arg const JK_ALL: int = 4 # Promise group: succeeds when every member does const JK_RACE: int = 5 # Promise group: succeeds when the first member does -var jb_ready: bool = false -var jb_state: words = null # J_* -var jb_kind: words = null # JK_* -var jb_result: words = null # the success value -var jb_error: words = null # the failure code -var jb_arg: words = null # compute input n -var jb_i: words = null # compute progress counter -var jb_acc: words = null # compute accumulator -var jb_acc2: words = null # compute second accumulator (Fibonacci) -var jb_nmem: words = null # group member count -var jb_mem: words = null # flat [JOB_SLOTS * JOB_MAXMEM] of member handles +export state RtJobsState { + jb_ready: bool = false + jb_state: words = null # J_* + jb_kind: words = null # JK_* + jb_result: words = null # the success value + jb_error: words = null # the failure code + jb_arg: words = null # compute input n + jb_i: words = null # compute progress counter + jb_acc: words = null # compute accumulator + jb_acc2: words = null # compute second accumulator (Fibonacci) + jb_nmem: words = null # group member count + jb_mem: words = null # flat [JOB_SLOTS * JOB_MAXMEM] of member handles + sy_ready: bool = false + mx_used: words = null + mx_obj: pointers = null # the native mutex behind each handle + at_used: words = null + at_cell: pointers = null # the int each atomic handle names (a words(1) of its own) + ch_used: words = null + ch_head: words = null + ch_count: words = null + ch_buf: words = null # flat [SYNC_CHAN * CHAN_CAP] + ch_lock: pointers = null # a mutex per channel +} -function jb_init() -> void { - if jb_ready { return } - jb_state = words(JOB_SLOTS); fill(jb_state, 0, JOB_SLOTS * 4) - jb_kind = words(JOB_SLOTS); fill(jb_kind, 0, JOB_SLOTS * 4) - jb_result = words(JOB_SLOTS); fill(jb_result, 0, JOB_SLOTS * 4) - jb_error = words(JOB_SLOTS); fill(jb_error, 0, JOB_SLOTS * 4) - jb_arg = words(JOB_SLOTS); fill(jb_arg, 0, JOB_SLOTS * 4) - jb_i = words(JOB_SLOTS); fill(jb_i, 0, JOB_SLOTS * 4) - jb_acc = words(JOB_SLOTS); fill(jb_acc, 0, JOB_SLOTS * 4) - jb_acc2 = words(JOB_SLOTS); fill(jb_acc2, 0, JOB_SLOTS * 4) - jb_nmem = words(JOB_SLOTS); fill(jb_nmem, 0, JOB_SLOTS * 4) - jb_mem = words(JOB_SLOTS * JOB_MAXMEM); fill(jb_mem, 0, JOB_SLOTS * JOB_MAXMEM * 4) - jb_ready = true +function jb_init(rt_jobs_st: mut RtJobsState) -> void { + if rt_jobs_st.jb_ready { return } + rt_jobs_st.jb_state = words(JOB_SLOTS); fill(rt_jobs_st.jb_state, 0, JOB_SLOTS * 4) + rt_jobs_st.jb_kind = words(JOB_SLOTS); fill(rt_jobs_st.jb_kind, 0, JOB_SLOTS * 4) + rt_jobs_st.jb_result = words(JOB_SLOTS); fill(rt_jobs_st.jb_result, 0, JOB_SLOTS * 4) + rt_jobs_st.jb_error = words(JOB_SLOTS); fill(rt_jobs_st.jb_error, 0, JOB_SLOTS * 4) + rt_jobs_st.jb_arg = words(JOB_SLOTS); fill(rt_jobs_st.jb_arg, 0, JOB_SLOTS * 4) + rt_jobs_st.jb_i = words(JOB_SLOTS); fill(rt_jobs_st.jb_i, 0, JOB_SLOTS * 4) + rt_jobs_st.jb_acc = words(JOB_SLOTS); fill(rt_jobs_st.jb_acc, 0, JOB_SLOTS * 4) + rt_jobs_st.jb_acc2 = words(JOB_SLOTS); fill(rt_jobs_st.jb_acc2, 0, JOB_SLOTS * 4) + rt_jobs_st.jb_nmem = words(JOB_SLOTS); fill(rt_jobs_st.jb_nmem, 0, JOB_SLOTS * 4) + rt_jobs_st.jb_mem = words(JOB_SLOTS * JOB_MAXMEM); fill(rt_jobs_st.jb_mem, 0, JOB_SLOTS * JOB_MAXMEM * 4) + rt_jobs_st.jb_ready = true } # claim a free slot as PENDING with the given kind; returns a 1-based handle, or # 0 if the table is full. -function jb_alloc(kind: int) -> int { - jb_init() +function jb_alloc(rt_jobs_st: mut RtJobsState, kind: int) -> int { + jb_init(rt_jobs_st) var i = 0 while i < JOB_SLOTS { - if jb_state[i] == J_FREE { - jb_state[i] = J_PENDING - jb_kind[i] = kind - jb_result[i] = 0; jb_error[i] = 0 - jb_arg[i] = 0; jb_i[i] = 0; jb_acc[i] = 0; jb_acc2[i] = 0 - jb_nmem[i] = 0 + if rt_jobs_st.jb_state[i] == J_FREE { + rt_jobs_st.jb_state[i] = J_PENDING + rt_jobs_st.jb_kind[i] = kind + rt_jobs_st.jb_result[i] = 0; rt_jobs_st.jb_error[i] = 0 + rt_jobs_st.jb_arg[i] = 0; rt_jobs_st.jb_i[i] = 0; rt_jobs_st.jb_acc[i] = 0; rt_jobs_st.jb_acc2[i] = 0 + rt_jobs_st.jb_nmem[i] = 0 return i + 1 } i += 1 @@ -95,71 +107,71 @@ function jb_alloc(kind: int) -> int { return 0 } -function jb_valid(h: int) -> bool { - jb_init() +function jb_valid(rt_jobs_st: mut RtJobsState, h: int) -> bool { + jb_init(rt_jobs_st) if (h < 1) or (h > JOB_SLOTS) { return false } - return jb_state[h - 1] != J_FREE + return rt_jobs_st.jb_state[h - 1] != J_FREE } # ---- the safe tier: futures ------------------------------------------------ # A hand-driven future: PENDING until you call Job.fulfill / Job.fail on it. -function job_defer() -> int { return jb_alloc(JK_DEFER) } +function job_defer(rt_jobs_st: mut RtJobsState) -> int { return jb_alloc(rt_jobs_st, JK_DEFER) } # Kick off a background compute job (kind = JK_SUM / JK_FIB / JK_PRIMES). It runs # a little each Job.pump and resolves when it finishes. `arg` is its input. -function job_run(kind: int, arg: int) -> int { - let h = jb_alloc(kind) +function job_run(rt_jobs_st: mut RtJobsState, kind: int, arg: int) -> int { + let h = jb_alloc(rt_jobs_st, kind) if h == 0 { return 0 } let s = h - 1 - jb_arg[s] = arg - if kind == JK_FIB { jb_acc[s] = 0; jb_acc2[s] = 1 } # fib(0)=0, fib(1)=1 + rt_jobs_st.jb_arg[s] = arg + if kind == JK_FIB { rt_jobs_st.jb_acc[s] = 0; rt_jobs_st.jb_acc2[s] = 1 } # fib(0)=0, fib(1)=1 return h } # Resolve a pending job successfully with `value` (no-op once resolved). -function job_fulfill(h: int, value: int) -> void { - if not jb_valid(h) { return } +function job_fulfill(rt_jobs_st: mut RtJobsState, h: int, value: int) -> void { + if not jb_valid(rt_jobs_st, h) { return } let s = h - 1 - if jb_state[s] != J_PENDING { return } - jb_state[s] = J_DONE - jb_result[s] = value + if rt_jobs_st.jb_state[s] != J_PENDING { return } + rt_jobs_st.jb_state[s] = J_DONE + rt_jobs_st.jb_result[s] = value } # Resolve a pending job as failed with error code `err` (no-op once resolved). -function job_fail(h: int, err: int) -> void { - if not jb_valid(h) { return } +function job_fail(rt_jobs_st: mut RtJobsState, h: int, err: int) -> void { + if not jb_valid(rt_jobs_st, h) { return } let s = h - 1 - if jb_state[s] != J_PENDING { return } - jb_state[s] = J_FAILED - jb_error[s] = err + if rt_jobs_st.jb_state[s] != J_PENDING { return } + rt_jobs_st.jb_state[s] = J_FAILED + rt_jobs_st.jb_error[s] = err } # Cancel a pending job (no-op if it already resolved). -function job_cancel(h: int) -> void { - if not jb_valid(h) { return } +function job_cancel(rt_jobs_st: mut RtJobsState, h: int) -> void { + if not jb_valid(rt_jobs_st, h) { return } let s = h - 1 - if jb_state[s] == J_PENDING { jb_state[s] = J_CANCELLED } + if rt_jobs_st.jb_state[s] == J_PENDING { rt_jobs_st.jb_state[s] = J_CANCELLED } } # Recompute a group job (JK_ALL / JK_RACE) from its members. A no-op unless the # slot is a still-PENDING group. This is what "resolve on the main thread" means: # a Promise settles only when you look at it (done/ok/...) or pump. -function jb_refresh_group(s: int) -> void { - if jb_state[s] != J_PENDING { return } - let k = jb_kind[s] +function jb_refresh_group(rt_jobs_st: mut RtJobsState, s: int) -> void { + if rt_jobs_st.jb_state[s] != J_PENDING { return } + let k = rt_jobs_st.jb_kind[s] if (k != JK_ALL) and (k != JK_RACE) { return } - let n = jb_nmem[s] + let n = rt_jobs_st.jb_nmem[s] let base = s * JOB_MAXMEM var i = 0 var settled = 0 # members in a terminal state var ok = 0 # members that succeeded var first_ok = 0 # winning handle for RACE while i < n { - let mh = jb_mem[base + i] - if jb_valid(mh) { + let mh = rt_jobs_st.jb_mem[base + i] + if jb_valid(rt_jobs_st, mh) { let ms = mh - 1 - let mst = jb_state[ms] + let mst = rt_jobs_st.jb_state[ms] if mst != J_PENDING { settled += 1 if mst == J_DONE { @@ -173,99 +185,99 @@ function jb_refresh_group(s: int) -> void { i += 1 } if k == JK_ALL { - if ok == n { jb_state[s] = J_DONE; jb_result[s] = n } - else { if settled == n { jb_state[s] = J_FAILED; jb_error[s] = n - ok } } + if ok == n { rt_jobs_st.jb_state[s] = J_DONE; rt_jobs_st.jb_result[s] = n } + else { if settled == n { rt_jobs_st.jb_state[s] = J_FAILED; rt_jobs_st.jb_error[s] = n - ok } } } else { - if first_ok != 0 { jb_state[s] = J_DONE; jb_result[s] = first_ok } - else { if settled == n { jb_state[s] = J_FAILED; jb_error[s] = n } } + if first_ok != 0 { rt_jobs_st.jb_state[s] = J_DONE; rt_jobs_st.jb_result[s] = first_ok } + else { if settled == n { rt_jobs_st.jb_state[s] = J_FAILED; rt_jobs_st.jb_error[s] = n } } } } # resolved in any terminal state? -function job_done(h: int) -> bool { - if not jb_valid(h) { return false } - jb_refresh_group(h - 1) - return jb_state[h - 1] != J_PENDING +function job_done(rt_jobs_st: mut RtJobsState, h: int) -> bool { + if not jb_valid(rt_jobs_st, h) { return false } + jb_refresh_group(rt_jobs_st, h - 1) + return rt_jobs_st.jb_state[h - 1] != J_PENDING } -function job_ok(h: int) -> bool { - if not jb_valid(h) { return false } - jb_refresh_group(h - 1) - return jb_state[h - 1] == J_DONE +function job_ok(rt_jobs_st: mut RtJobsState, h: int) -> bool { + if not jb_valid(rt_jobs_st, h) { return false } + jb_refresh_group(rt_jobs_st, h - 1) + return rt_jobs_st.jb_state[h - 1] == J_DONE } -function job_failed(h: int) -> bool { - if not jb_valid(h) { return false } - jb_refresh_group(h - 1) - return jb_state[h - 1] == J_FAILED +function job_failed(rt_jobs_st: mut RtJobsState, h: int) -> bool { + if not jb_valid(rt_jobs_st, h) { return false } + jb_refresh_group(rt_jobs_st, h - 1) + return rt_jobs_st.jb_state[h - 1] == J_FAILED } -function job_cancelled(h: int) -> bool { - if not jb_valid(h) { return false } - return jb_state[h - 1] == J_CANCELLED +function job_cancelled(rt_jobs_st: mut RtJobsState, h: int) -> bool { + if not jb_valid(rt_jobs_st, h) { return false } + return rt_jobs_st.jb_state[h - 1] == J_CANCELLED } # the success value (0 unless the job is done-ok) -function job_result(h: int) -> int { - if not jb_valid(h) { return 0 } - jb_refresh_group(h - 1) - if jb_state[h - 1] != J_DONE { return 0 } - return jb_result[h - 1] +function job_result(rt_jobs_st: mut RtJobsState, h: int) -> int { + if not jb_valid(rt_jobs_st, h) { return 0 } + jb_refresh_group(rt_jobs_st, h - 1) + if rt_jobs_st.jb_state[h - 1] != J_DONE { return 0 } + return rt_jobs_st.jb_result[h - 1] } # the failure code (0 unless the job failed) -function job_error(h: int) -> int { - if not jb_valid(h) { return 0 } - jb_refresh_group(h - 1) - if jb_state[h - 1] != J_FAILED { return 0 } - return jb_error[h - 1] +function job_error(rt_jobs_st: mut RtJobsState, h: int) -> int { + if not jb_valid(rt_jobs_st, h) { return 0 } + jb_refresh_group(rt_jobs_st, h - 1) + if rt_jobs_st.jb_state[h - 1] != J_FAILED { return 0 } + return rt_jobs_st.jb_error[h - 1] } # how many jobs are still pending (a ready-made loading-screen denominator). -function job_pending() -> int { - jb_init() +function job_pending(rt_jobs_st: mut RtJobsState) -> int { + jb_init(rt_jobs_st) var n = 0 var i = 0 while i < JOB_SLOTS { - if jb_state[i] == J_PENDING { n += 1 } + if rt_jobs_st.jb_state[i] == J_PENDING { n += 1 } i += 1 } return n } # release a slot back to the pool. -function job_free(h: int) -> void { - if not jb_valid(h) { return } - jb_state[h - 1] = J_FREE +function job_free(rt_jobs_st: mut RtJobsState, h: int) -> void { + if not jb_valid(rt_jobs_st, h) { return } + rt_jobs_st.jb_state[h - 1] = J_FREE } # advance one compute job by a single step; returns 1 if it just finished. -function jb_step(s: int) -> int { - let k = jb_kind[s] - let n = jb_arg[s] - var i = jb_i[s] +function jb_step(rt_jobs_st: mut RtJobsState, s: int) -> int { + let k = rt_jobs_st.jb_kind[s] + let n = rt_jobs_st.jb_arg[s] + var i = rt_jobs_st.jb_i[s] if k == JK_SUM { - jb_acc[s] = jb_acc[s] + (i + 1) + rt_jobs_st.jb_acc[s] = rt_jobs_st.jb_acc[s] + (i + 1) i += 1 - jb_i[s] = i - if i >= n { jb_state[s] = J_DONE; jb_result[s] = jb_acc[s]; return 1 } + rt_jobs_st.jb_i[s] = i + if i >= n { rt_jobs_st.jb_state[s] = J_DONE; rt_jobs_st.jb_result[s] = rt_jobs_st.jb_acc[s]; return 1 } return 0 } if k == JK_FIB { - if i >= n { jb_state[s] = J_DONE; jb_result[s] = jb_acc[s]; return 1 } - let t = jb_acc[s] + jb_acc2[s] - jb_acc[s] = jb_acc2[s] - jb_acc2[s] = t + if i >= n { rt_jobs_st.jb_state[s] = J_DONE; rt_jobs_st.jb_result[s] = rt_jobs_st.jb_acc[s]; return 1 } + let t = rt_jobs_st.jb_acc[s] + rt_jobs_st.jb_acc2[s] + rt_jobs_st.jb_acc[s] = rt_jobs_st.jb_acc2[s] + rt_jobs_st.jb_acc2[s] = t i += 1 - jb_i[s] = i - if i >= n { jb_state[s] = J_DONE; jb_result[s] = jb_acc[s]; return 1 } + rt_jobs_st.jb_i[s] = i + if i >= n { rt_jobs_st.jb_state[s] = J_DONE; rt_jobs_st.jb_result[s] = rt_jobs_st.jb_acc[s]; return 1 } return 0 } if k == JK_PRIMES { - if jb_is_prime(i) { jb_acc[s] += 1 } + if jb_is_prime(i) { rt_jobs_st.jb_acc[s] += 1 } i += 1 - jb_i[s] = i - if i > n { jb_state[s] = J_DONE; jb_result[s] = jb_acc[s]; return 1 } + rt_jobs_st.jb_i[s] = i + if i > n { rt_jobs_st.jb_state[s] = J_DONE; rt_jobs_st.jb_result[s] = rt_jobs_st.jb_acc[s]; return 1 } return 0 } return 0 @@ -285,17 +297,17 @@ function jb_is_prime(v: int) -> bool { # resolve any Promise groups. Call it once per frame (or wherever you want the # results to land). Returns how many jobs finished during this call. `budget` <= # 0 means "run every compute job to completion right now". -function job_pump(budget: int) -> int { - jb_init() +function job_pump(rt_jobs_st: mut RtJobsState, budget: int) -> int { + jb_init(rt_jobs_st) var completed = 0 var spent = 0 var s = 0 while s < JOB_SLOTS { - let k = jb_kind[s] + let k = rt_jobs_st.jb_kind[s] let compute = (k == JK_SUM) or (k == JK_FIB) or (k == JK_PRIMES) - while (jb_state[s] == J_PENDING) and compute { + while (rt_jobs_st.jb_state[s] == J_PENDING) and compute { if (budget > 0) and (spent >= budget) { s = JOB_SLOTS + 1; break } - let fin = jb_step(s) + let fin = jb_step(rt_jobs_st, s) spent += 1 if fin == 1 { completed += 1 } } @@ -304,11 +316,11 @@ function job_pump(budget: int) -> int { # settle groups after the compute jobs advanced this frame. s = 0 while s < JOB_SLOTS { - if jb_state[s] == J_PENDING { - let k = jb_kind[s] + if rt_jobs_st.jb_state[s] == J_PENDING { + let k = rt_jobs_st.jb_kind[s] if (k == JK_ALL) or (k == JK_RACE) { - jb_refresh_group(s) - if jb_state[s] != J_PENDING { completed += 1 } + jb_refresh_group(rt_jobs_st, s) + if rt_jobs_st.jb_state[s] != J_PENDING { completed += 1 } } } s += 1 @@ -319,51 +331,51 @@ function job_pump(budget: int) -> int { # ---- Promise combinators (over a []int of job handles) --------------------- # store up to JOB_MAXMEM handles as the members of group slot `s`. -function jb_set_members(s: int, handles: []int) -> void { +function jb_set_members(rt_jobs_st: mut RtJobsState, s: int, handles: []int) -> void { var n = len(handles) if n > JOB_MAXMEM { n = JOB_MAXMEM } let base = s * JOB_MAXMEM var i = 0 - while i < n { jb_mem[base + i] = handles[i]; i += 1 } - jb_nmem[s] = n + while i < n { rt_jobs_st.jb_mem[base + i] = handles[i]; i += 1 } + rt_jobs_st.jb_nmem[s] = n } # A promise that succeeds once every member has succeeded, and fails as soon as # the whole set has settled with at least one non-success. Returns a job handle. -function prom_all(handles: []int) -> int { - let h = jb_alloc(JK_ALL) +function prom_all(rt_jobs_st: mut RtJobsState, handles: []int) -> int { + let h = jb_alloc(rt_jobs_st, JK_ALL) if h == 0 { return 0 } - jb_set_members(h - 1, handles) - jb_refresh_group(h - 1) + jb_set_members(rt_jobs_st, h - 1, handles) + jb_refresh_group(rt_jobs_st, h - 1) return h } # A promise that succeeds as soon as the first member succeeds (its handle is the # result), and fails only if every member settles without success. -function prom_race(handles: []int) -> int { - let h = jb_alloc(JK_RACE) +function prom_race(rt_jobs_st: mut RtJobsState, handles: []int) -> int { + let h = jb_alloc(rt_jobs_st, JK_RACE) if h == 0 { return 0 } - jb_set_members(h - 1, handles) - jb_refresh_group(h - 1) + jb_set_members(rt_jobs_st, h - 1, handles) + jb_refresh_group(rt_jobs_st, h - 1) return h } # how many of `handles` have resolved (any terminal state) — a loading bar's # numerator; pair with len(handles) for the denominator. -function prom_count_done(handles: []int) -> int { +function prom_count_done(rt_jobs_st: mut RtJobsState, handles: []int) -> int { var n = 0 var i = 0 while i < len(handles) { - if job_done(handles[i]) { n += 1 } + if job_done(rt_jobs_st, handles[i]) { n += 1 } i += 1 } return n } -function prom_all_done(handles: []int) -> bool { +function prom_all_done(rt_jobs_st: mut RtJobsState, handles: []int) -> bool { var i = 0 while i < len(handles) { - if not job_done(handles[i]) { return false } + if not job_done(rt_jobs_st, handles[i]) { return false } i += 1 } return true @@ -401,39 +413,29 @@ extern function thr_cas(p: pointer, expect: int, next: int) -> int = "thr_cas" extern function thr_load(p: pointer) -> int = "thr_load" extern function thr_store(p: pointer, v: int) = "thr_store" -var sy_ready: bool = false -var mx_used: words = null -var mx_obj: pointers = null # the native mutex behind each handle -var at_used: words = null -var at_cell: pointers = null # the int each atomic handle names (a words(1) of its own) -var ch_used: words = null -var ch_head: words = null -var ch_count: words = null -var ch_buf: words = null # flat [SYNC_CHAN * CHAN_CAP] -var ch_lock: pointers = null # a mutex per channel -function sy_init() -> void { - if sy_ready { return } - mx_used = words(SYNC_MUTEX); fill(mx_used, 0, SYNC_MUTEX * 4) - mx_obj = pointers(SYNC_MUTEX); fill(mx_obj, 0, SYNC_MUTEX * 8) - at_used = words(SYNC_ATOMIC); fill(at_used, 0, SYNC_ATOMIC * 4) - at_cell = pointers(SYNC_ATOMIC); fill(at_cell, 0, SYNC_ATOMIC * 8) - ch_used = words(SYNC_CHAN); fill(ch_used, 0, SYNC_CHAN * 4) - ch_head = words(SYNC_CHAN); fill(ch_head, 0, SYNC_CHAN * 4) - ch_count = words(SYNC_CHAN); fill(ch_count, 0, SYNC_CHAN * 4) - ch_buf = words(SYNC_CHAN * CHAN_CAP); fill(ch_buf, 0, SYNC_CHAN * CHAN_CAP * 4) - ch_lock = pointers(SYNC_CHAN); fill(ch_lock, 0, SYNC_CHAN * 8) - sy_ready = true +function sy_init(rt_jobs_st: mut RtJobsState) -> void { + if rt_jobs_st.sy_ready { return } + rt_jobs_st.mx_used = words(SYNC_MUTEX); fill(rt_jobs_st.mx_used, 0, SYNC_MUTEX * 4) + rt_jobs_st.mx_obj = pointers(SYNC_MUTEX); fill(rt_jobs_st.mx_obj, 0, SYNC_MUTEX * 8) + rt_jobs_st.at_used = words(SYNC_ATOMIC); fill(rt_jobs_st.at_used, 0, SYNC_ATOMIC * 4) + rt_jobs_st.at_cell = pointers(SYNC_ATOMIC); fill(rt_jobs_st.at_cell, 0, SYNC_ATOMIC * 8) + rt_jobs_st.ch_used = words(SYNC_CHAN); fill(rt_jobs_st.ch_used, 0, SYNC_CHAN * 4) + rt_jobs_st.ch_head = words(SYNC_CHAN); fill(rt_jobs_st.ch_head, 0, SYNC_CHAN * 4) + rt_jobs_st.ch_count = words(SYNC_CHAN); fill(rt_jobs_st.ch_count, 0, SYNC_CHAN * 4) + rt_jobs_st.ch_buf = words(SYNC_CHAN * CHAN_CAP); fill(rt_jobs_st.ch_buf, 0, SYNC_CHAN * CHAN_CAP * 4) + rt_jobs_st.ch_lock = pointers(SYNC_CHAN); fill(rt_jobs_st.ch_lock, 0, SYNC_CHAN * 8) + rt_jobs_st.sy_ready = true } # ---- mutex ----------------------------------------------------------------- -function sync_mutex() -> int { - sy_init() +function sync_mutex(rt_jobs_st: mut RtJobsState) -> int { + sy_init(rt_jobs_st) var i = 0 while i < SYNC_MUTEX { - if mx_used[i] == 0 { - mx_used[i] = 1 - if mx_obj[i] == null { mx_obj[i] = thr_mutex_new() } + if rt_jobs_st.mx_used[i] == 0 { + rt_jobs_st.mx_used[i] = 1 + if rt_jobs_st.mx_obj[i] == null { rt_jobs_st.mx_obj[i] = thr_mutex_new() } return i + 1 } i += 1 @@ -441,34 +443,34 @@ function sync_mutex() -> int { return 0 } -function sync_lock(m: int) -> void { - sy_init() +function sync_lock(rt_jobs_st: mut RtJobsState, m: int) -> void { + sy_init(rt_jobs_st) if (m < 1) or (m > SYNC_MUTEX) { return } - thr_lock(mx_obj[m - 1]) + thr_lock(rt_jobs_st.mx_obj[m - 1]) } -function sync_unlock(m: int) -> void { - sy_init() +function sync_unlock(rt_jobs_st: mut RtJobsState, m: int) -> void { + sy_init(rt_jobs_st) if (m < 1) or (m > SYNC_MUTEX) { return } - thr_unlock(mx_obj[m - 1]) + thr_unlock(rt_jobs_st.mx_obj[m - 1]) } # take the lock only if it is free; returns whether it was taken. -function sync_try_lock(m: int) -> bool { - sy_init() +function sync_try_lock(rt_jobs_st: mut RtJobsState, m: int) -> bool { + sy_init(rt_jobs_st) if (m < 1) or (m > SYNC_MUTEX) { return false } - return thr_trylock(mx_obj[m - 1]) == 1 + return thr_trylock(rt_jobs_st.mx_obj[m - 1]) == 1 } # ---- atomic counter -------------------------------------------------------- -function sync_atomic() -> int { - sy_init() +function sync_atomic(rt_jobs_st: mut RtJobsState) -> int { + sy_init(rt_jobs_st) var i = 0 while i < SYNC_ATOMIC { - if at_used[i] == 0 { - at_used[i] = 1 - if at_cell[i] == null { at_cell[i] = data_of(words(1)) } # the atomics take the cell's address, not its slice - thr_store(at_cell[i], 0) + if rt_jobs_st.at_used[i] == 0 { + rt_jobs_st.at_used[i] = 1 + if rt_jobs_st.at_cell[i] == null { rt_jobs_st.at_cell[i] = data_of(words(1)) } # the atomics take the cell's address, not its slice + thr_store(rt_jobs_st.at_cell[i], 0) return i + 1 } i += 1 @@ -476,40 +478,40 @@ function sync_atomic() -> int { return 0 } -function sync_get(a: int) -> int { - sy_init() +function sync_get(rt_jobs_st: mut RtJobsState, a: int) -> int { + sy_init(rt_jobs_st) if (a < 1) or (a > SYNC_ATOMIC) { return 0 } - return thr_load(at_cell[a - 1]) + return thr_load(rt_jobs_st.at_cell[a - 1]) } -function sync_set(a: int, v: int) -> void { - sy_init() +function sync_set(rt_jobs_st: mut RtJobsState, a: int, v: int) -> void { + sy_init(rt_jobs_st) if (a < 1) or (a > SYNC_ATOMIC) { return } - thr_store(at_cell[a - 1], v) + thr_store(rt_jobs_st.at_cell[a - 1], v) } # add `delta` and return the new value. -function sync_add(a: int, delta: int) -> int { - sy_init() +function sync_add(rt_jobs_st: mut RtJobsState, a: int, delta: int) -> int { + sy_init(rt_jobs_st) if (a < 1) or (a > SYNC_ATOMIC) { return 0 } - return thr_atomic_add(at_cell[a - 1], delta) + return thr_atomic_add(rt_jobs_st.at_cell[a - 1], delta) } # compare-and-set: if the value equals `expect`, store `next` and return true. -function sync_cas(a: int, expect: int, next: int) -> bool { - sy_init() +function sync_cas(rt_jobs_st: mut RtJobsState, a: int, expect: int, next: int) -> bool { + sy_init(rt_jobs_st) if (a < 1) or (a > SYNC_ATOMIC) { return false } - return thr_cas(at_cell[a - 1], expect, next) == 1 + return thr_cas(rt_jobs_st.at_cell[a - 1], expect, next) == 1 } # ---- channel (a bounded int FIFO, behind its own mutex) --------------------- -function sync_channel() -> int { - sy_init() +function sync_channel(rt_jobs_st: mut RtJobsState) -> int { + sy_init(rt_jobs_st) var i = 0 while i < SYNC_CHAN { - if ch_used[i] == 0 { - ch_used[i] = 1; ch_head[i] = 0; ch_count[i] = 0 - if ch_lock[i] == null { ch_lock[i] = thr_mutex_new() } + if rt_jobs_st.ch_used[i] == 0 { + rt_jobs_st.ch_used[i] = 1; rt_jobs_st.ch_head[i] = 0; rt_jobs_st.ch_count[i] = 0 + if rt_jobs_st.ch_lock[i] == null { rt_jobs_st.ch_lock[i] = thr_mutex_new() } return i + 1 } i += 1 @@ -518,52 +520,52 @@ function sync_channel() -> int { } # enqueue `v`; returns false if the channel is full. -function sync_send(c: int, v: int) -> bool { - sy_init() +function sync_send(rt_jobs_st: mut RtJobsState, c: int, v: int) -> bool { + sy_init(rt_jobs_st) if (c < 1) or (c > SYNC_CHAN) { return false } let s = c - 1 - thr_lock(ch_lock[s]) - if ch_count[s] >= CHAN_CAP { thr_unlock(ch_lock[s]); return false } - let pos = ch_head[s] + ch_count[s] + thr_lock(rt_jobs_st.ch_lock[s]) + if rt_jobs_st.ch_count[s] >= CHAN_CAP { thr_unlock(rt_jobs_st.ch_lock[s]); return false } + let pos = rt_jobs_st.ch_head[s] + rt_jobs_st.ch_count[s] var idx = pos if idx >= CHAN_CAP { idx -= CHAN_CAP } - ch_buf[s * CHAN_CAP + idx] = v - ch_count[s] += 1 - thr_unlock(ch_lock[s]) + rt_jobs_st.ch_buf[s * CHAN_CAP + idx] = v + rt_jobs_st.ch_count[s] += 1 + thr_unlock(rt_jobs_st.ch_lock[s]) return true } # dequeue the oldest value; returns 0 on an empty channel (guard with can_recv). -function sync_recv(c: int) -> int { - sy_init() +function sync_recv(rt_jobs_st: mut RtJobsState, c: int) -> int { + sy_init(rt_jobs_st) if (c < 1) or (c > SYNC_CHAN) { return 0 } let s = c - 1 - thr_lock(ch_lock[s]) - if ch_count[s] == 0 { thr_unlock(ch_lock[s]); return 0 } - let v = ch_buf[s * CHAN_CAP + ch_head[s]] - var nh = ch_head[s] + 1 + thr_lock(rt_jobs_st.ch_lock[s]) + if rt_jobs_st.ch_count[s] == 0 { thr_unlock(rt_jobs_st.ch_lock[s]); return 0 } + let v = rt_jobs_st.ch_buf[s * CHAN_CAP + rt_jobs_st.ch_head[s]] + var nh = rt_jobs_st.ch_head[s] + 1 if nh >= CHAN_CAP { nh = 0 } - ch_head[s] = nh - ch_count[s] -= 1 - thr_unlock(ch_lock[s]) + rt_jobs_st.ch_head[s] = nh + rt_jobs_st.ch_count[s] -= 1 + thr_unlock(rt_jobs_st.ch_lock[s]) return v } -function sync_can_recv(c: int) -> bool { - sy_init() +function sync_can_recv(rt_jobs_st: mut RtJobsState, c: int) -> bool { + sy_init(rt_jobs_st) if (c < 1) or (c > SYNC_CHAN) { return false } - thr_lock(ch_lock[c - 1]) - let has = ch_count[c - 1] > 0 - thr_unlock(ch_lock[c - 1]) + thr_lock(rt_jobs_st.ch_lock[c - 1]) + let has = rt_jobs_st.ch_count[c - 1] > 0 + thr_unlock(rt_jobs_st.ch_lock[c - 1]) return has } -function sync_len(c: int) -> int { - sy_init() +function sync_len(rt_jobs_st: mut RtJobsState, c: int) -> int { + sy_init(rt_jobs_st) if (c < 1) or (c > SYNC_CHAN) { return 0 } - thr_lock(ch_lock[c - 1]) - let n = ch_count[c - 1] - thr_unlock(ch_lock[c - 1]) + thr_lock(rt_jobs_st.ch_lock[c - 1]) + let n = rt_jobs_st.ch_count[c - 1] + thr_unlock(rt_jobs_st.ch_lock[c - 1]) return n } diff --git a/runtime/native/light.ludic b/runtime/native/light.ludic index ca919f33..5b5ecf75 100644 --- a/runtime/native/light.ludic +++ b/runtime/native/light.ludic @@ -34,26 +34,34 @@ # Up to 64 rectangular occluders, stored flat as (x, y, w, h) i32 quads. A game # clears them each frame (Light.clear_occluders) and re-registers the geometry # that should cast shadows this frame. -var rt_light_occ: words = null # occluder rects: 4 i32 each — x, y, w, h -var rt_light_occ_n: int = 0 # number of occluders currently stored +export state RtLightState { + rt_light_occ: words = null # occluder rects: 4 i32 each — x, y, w, h + rt_light_occ_n: int = 0 # number of occluders currently stored + rt_light_falloff: int = 1 # brightness ramp exponent: 1 linear, 2 quadratic, 3 cubic… + rt_light_soft: int = 0 # penumbra radius in px (0 = hard single-sample shadow) + rt_light_gel: int = 0 # outer gel colour 0x00RRGGBB (used only when rt_light_gel_on) + rt_light_gel_on: bool = false # gel active? off = a flat single-colour light + rt_light_h: int = 64 # virtual light height above the surface, for normal-map N·L + rt_light_nrm: words = null +} -function light_occ_init() -> void { - if rt_light_occ == null { rt_light_occ = words(64 * 4) } +function light_occ_init(rt_light_st: mut RtLightState) -> void { + if rt_light_st.rt_light_occ == null { rt_light_st.rt_light_occ = words(64 * 4) } } # forget every occluder — call once per frame before re-registering geometry. -function light_clear_occluders() -> void { rt_light_occ_n = 0 } +function light_clear_occluders(rt_light_st: mut RtLightState) -> void { rt_light_st.rt_light_occ_n = 0 } # register a rectangular shadow caster (screen space). Silently ignored past 64. -function light_occlude(x: int, y: int, w: int, h: int) -> void { - light_occ_init() - if rt_light_occ_n >= 64 { return } - let i = rt_light_occ_n * 4 - rt_light_occ[i] = x - rt_light_occ[i + 1] = y - rt_light_occ[i + 2] = w - rt_light_occ[i + 3] = h - rt_light_occ_n += 1 +function light_occlude(rt_light_st: mut RtLightState, x: int, y: int, w: int, h: int) -> void { + light_occ_init(rt_light_st) + if rt_light_st.rt_light_occ_n >= 64 { return } + let i = rt_light_st.rt_light_occ_n * 4 + rt_light_st.rt_light_occ[i] = x + rt_light_st.rt_light_occ[i + 1] = y + rt_light_st.rt_light_occ[i + 2] = w + rt_light_st.rt_light_occ[i + 3] = h + rt_light_st.rt_light_occ_n += 1 } # ---- shadow geometry ------------------------------------------------------ @@ -85,14 +93,14 @@ function light_pt_in_rect(px: int, py: int, rx: int, ry: int, rw: int, rh: int) # is the segment from light (lx,ly) to pixel (px,py) blocked by any occluder? # A pixel inside an occluder is in shadow; otherwise the ray is blocked if it # crosses any of the rectangle's four edges. -function light_blocked(lx: int, ly: int, px: int, py: int) -> bool { +function light_blocked(rt_light_st: RtLightState, lx: int, ly: int, px: int, py: int) -> bool { var k = 0 - while k < rt_light_occ_n { + while k < rt_light_st.rt_light_occ_n { let i = k * 4 - let rx = rt_light_occ[i] - let ry = rt_light_occ[i + 1] - let rw = rt_light_occ[i + 2] - let rh = rt_light_occ[i + 3] + let rx = rt_light_st.rt_light_occ[i] + let ry = rt_light_st.rt_light_occ[i + 1] + let rw = rt_light_st.rt_light_occ[i + 2] + let rh = rt_light_st.rt_light_occ[i + 3] if light_pt_in_rect(px, py, rx, ry, rw, rh) { return true } let x0 = rx let y0 = ry @@ -132,19 +140,14 @@ function light_fsqrt(v: fixed) -> fixed { # occluder store — a game (or the engine ECS system) sets them before emitting a # light and they stay until changed, so the simple Light.point call keeps its # short signature while spot/soft/falloff/gel ride on this side-band state. -var rt_light_falloff: int = 1 # brightness ramp exponent: 1 linear, 2 quadratic, 3 cubic… -var rt_light_soft: int = 0 # penumbra radius in px (0 = hard single-sample shadow) -var rt_light_gel: int = 0 # outer gel colour 0x00RRGGBB (used only when rt_light_gel_on) -var rt_light_gel_on: bool = false # gel active? off = a flat single-colour light -var rt_light_h: int = 64 # virtual light height above the surface, for normal-map N·L -function light_set_falloff(exp: int) -> void { - if exp < 1 { rt_light_falloff = 1 } else { rt_light_falloff = exp } +function light_set_falloff(rt_light_st: mut RtLightState, exp: int) -> void { + if exp < 1 { rt_light_st.rt_light_falloff = 1 } else { rt_light_st.rt_light_falloff = exp } } -function light_set_soft(radius: int) -> void { rt_light_soft = radius } -function light_set_gel(outer: int) -> void { rt_light_gel = outer; rt_light_gel_on = true } -function light_clear_gel() -> void { rt_light_gel_on = false } -function light_set_height(h: int) -> void { if h > 0 { rt_light_h = h } } +function light_set_soft(rt_light_st: mut RtLightState, radius: int) -> void { rt_light_st.rt_light_soft = radius } +function light_set_gel(rt_light_st: mut RtLightState, outer: int) -> void { rt_light_st.rt_light_gel = outer; rt_light_st.rt_light_gel_on = true } +function light_clear_gel(rt_light_st: mut RtLightState) -> void { rt_light_st.rt_light_gel_on = false } +function light_set_height(rt_light_st: mut RtLightState, h: int) -> void { if h > 0 { rt_light_st.rt_light_h = h } } # ---- normal buffer -------------------------------------------------------- # An optional per-pixel surface-normal G-buffer, parallel to the framebuffer. @@ -152,25 +155,24 @@ function light_set_height(h: int) -> void { if h > 0 { rt_light_h = h } } # here" so an unstamped scene lights exactly as before (factor 1). nz is recovered # from the unit constraint, so a Light2D shades a surface by the angle it faces, # not distance alone (tier 3). Allocated only when a game stamps a normal. -var rt_light_nrm: words = null -function light_nrm_init() -> void { - if rt_light_nrm == null { rt_light_nrm = words(rt_fbw * rt_fbh) } +function light_nrm_init(rt_light_st: mut RtLightState) -> void { + if rt_light_st.rt_light_nrm == null { rt_light_st.rt_light_nrm = words(rt_fbw * rt_fbh) } } # forget every stamped normal — call once per frame before re-stamping surfaces. -function light_clear_normals() -> void { - if rt_light_nrm == null { return } +function light_clear_normals(rt_light_st: mut RtLightState) -> void { + if rt_light_st.rt_light_nrm == null { return } let n = rt_fbw * rt_fbh var i = 0 - while i < n { rt_light_nrm[i] = 0; i += 1 } + while i < n { rt_light_st.rt_light_nrm[i] = 0; i += 1 } } # stamp a rectangular region's surface normal. nx, ny are the normal's x/y as a # Q16.16 fixed in [-1, 1] (a flat surface facing the camera is nx = ny = 0); nz is # derived. Screen space, clipped to the framebuffer. -function light_normal_rect(x: int, y: int, w: int, h: int, nx: fixed, ny: fixed) -> void { - light_nrm_init() +function light_normal_rect(rt_light_st: mut RtLightState, x: int, y: int, w: int, h: int, nx: fixed, ny: fixed) -> void { + light_nrm_init(rt_light_st) var nxq = floor(nx * fixed(127)) + 128 var nyq = floor(ny * fixed(127)) + 128 if nxq < 0 { nxq = 0 }; if nxq > 255 { nxq = 255 } @@ -181,7 +183,7 @@ function light_normal_rect(x: int, y: int, w: int, h: int, nx: fixed, ny: fixed) if (py >= 0) and (py < rt_fbh) { var px = x while px < x + w { - if (px >= 0) and (px < rt_fbw) { rt_light_nrm[py * rt_fbw + px] = packed } + if (px >= 0) and (px < rt_fbw) { rt_light_st.rt_light_nrm[py * rt_fbw + px] = packed } px += 1 } } @@ -239,33 +241,33 @@ function light_pow_t(t: fixed, exp: int) -> fixed { # only its centre (0 or 1); a soft light (rt_light_soft > 0) samples a small cross # on the light disk and averages, so an occluder edge fades through a penumbra # instead of cutting sharply (tier 4). -function light_vis(cx: int, cy: int, px: int, py: int) -> fixed { - if rt_light_soft <= 0 { - if light_blocked(cx, cy, px, py) { return fixed(0) } +function light_vis(rt_light_st: RtLightState, cx: int, cy: int, px: int, py: int) -> fixed { + if rt_light_st.rt_light_soft <= 0 { + if light_blocked(rt_light_st, cx, cy, px, py) { return fixed(0) } return fixed(1) } - let s = rt_light_soft + let s = rt_light_st.rt_light_soft var hit = 0 - if not light_blocked(cx, cy, px, py) { hit += 1 } - if not light_blocked(cx + s, cy, px, py) { hit += 1 } - if not light_blocked(cx - s, cy, px, py) { hit += 1 } - if not light_blocked(cx, cy + s, px, py) { hit += 1 } - if not light_blocked(cx, cy - s, px, py) { hit += 1 } + if not light_blocked(rt_light_st, cx, cy, px, py) { hit += 1 } + if not light_blocked(rt_light_st, cx + s, cy, px, py) { hit += 1 } + if not light_blocked(rt_light_st, cx - s, cy, px, py) { hit += 1 } + if not light_blocked(rt_light_st, cx, cy + s, px, py) { hit += 1 } + if not light_blocked(rt_light_st, cx, cy - s, px, py) { hit += 1 } return fixed(hit) / fixed(5) } # Lambert factor at a pixel from the normal G-buffer, 0..1 (Q16.16). Flat / no # normal -> 1 (unchanged). Otherwise N·L with L the (normalized) direction from # the surface to the light in 3D, the light lifted rt_light_h above the plane. -function light_normal_factor(cx: int, cy: int, px: int, py: int) -> fixed { - if rt_light_nrm == null { return fixed(1) } - let packed = rt_light_nrm[py * rt_fbw + px] +function light_normal_factor(rt_light_st: RtLightState, cx: int, cy: int, px: int, py: int) -> fixed { + if rt_light_st.rt_light_nrm == null { return fixed(1) } + let packed = rt_light_st.rt_light_nrm[py * rt_fbw + px] if packed == 0 { return fixed(1) } let nxq = (packed & 255) - 128 let nyq = ((packed >> 8) & 255) - 128 let lx = cx - px let ly = cy - py - let lz = rt_light_h + let lz = rt_light_st.rt_light_h let lm = light_isqrt(lx * lx + ly * ly + lz * lz) if lm <= 0 { return fixed(1) } let lxf = fixed(lx) / fixed(lm) @@ -285,18 +287,18 @@ function light_normal_factor(cx: int, cy: int, px: int, py: int) -> fixed { # Multiply the whole scene by an ambient tint (0x00RRGGBB): the CanvasModulate # that gives a night/cave mood before any light adds brightness back. Ambient # 0xFFFFFF is a no-op; darker/colored tints dim and gel the scene. -function light_ambient(color: int) -> void { +function light_ambient(rt_core_st: mut RtCoreState, color: int) -> void { let ar = (color >> 16) & 255 let ag = (color >> 8) & 255 let ab = color & 255 let n = rt_fbw * rt_fbh var i = 0 while i < n { - let cur = rt_fb[i] + let cur = rt_core_st.rt_fb[i] let nr = (((cur >> 16) & 255) * ar) / 255 let ng = (((cur >> 8) & 255) * ag) / 255 let nb = ((cur & 255) * ab) / 255 - rt_fb[i] = (nr << 16) | (ng << 8) | nb + rt_core_st.rt_fb[i] = (nr << 16) | (ng << 8) | nb i += 1 } } @@ -315,14 +317,14 @@ function light_ambient(color: int) -> void { # Brightness is clamped per channel at 255; only the bounding box is touched. const LIGHT_FEATHER: int = 6 # cone-edge softening, in degrees -function light_emit(cx: int, cy: int, radius: int, color: int, energy: fixed, dir_deg: int, spread_deg: int) -> void { +function light_emit(rt_core_st: mut RtCoreState, rt_light_st: RtLightState, cx: int, cy: int, radius: int, color: int, energy: fixed, dir_deg: int, spread_deg: int) -> void { if radius <= 0 { return } let lr = (color >> 16) & 255 let lg = (color >> 8) & 255 let lb = color & 255 - let gr = (rt_light_gel >> 16) & 255 - let gg = (rt_light_gel >> 8) & 255 - let gb = rt_light_gel & 255 + let gr = (rt_light_st.rt_light_gel >> 16) & 255 + let gg = (rt_light_st.rt_light_gel >> 8) & 255 + let gb = rt_light_st.rt_light_gel & 255 var py = cy - radius while py <= cy + radius { if (py >= 0) and (py < rt_fbh) { @@ -344,26 +346,26 @@ function light_emit(cx: int, cy: int, radius: int, color: int, energy: fixed, di } } if cone > 0 { - let vis = light_vis(cx, cy, px, py) + let vis = light_vis(rt_light_st, cx, cy, px, py) if vis > 0 { let t_lin = fixed(radius - d) / fixed(radius) # 1 at centre, 0 at rim - let atten = light_pow_t(t_lin, rt_light_falloff) - let nf = light_normal_factor(cx, cy, px, py) + let atten = light_pow_t(t_lin, rt_light_st.rt_light_falloff) + let nf = light_normal_factor(rt_light_st, cx, cy, px, py) let k = atten * energy * vis * cone * nf # gel: mix the light colour toward the rim colour by (1 - t_lin). var cr = lr; var cg = lg; var cb = lb - if rt_light_gel_on { + if rt_light_st.rt_light_gel_on { let mix = fixed(1) - t_lin cr = lr + floor(fixed(gr - lr) * mix) cg = lg + floor(fixed(gg - lg) * mix) cb = lb + floor(fixed(gb - lb) * mix) } let idx = py * rt_fbw + px - let cur = rt_fb[idx] + let cur = rt_core_st.rt_fb[idx] let nr = min(255, ((cur >> 16) & 255) + floor(fixed(cr) * k)) let ng = min(255, ((cur >> 8) & 255) + floor(fixed(cg) * k)) let nb = min(255, (cur & 255) + floor(fixed(cb) * k)) - rt_fb[idx] = (nr << 16) | (ng << 8) | nb + rt_core_st.rt_fb[idx] = (nr << 16) | (ng << 8) | nb } } } @@ -377,15 +379,15 @@ function light_emit(cx: int, cy: int, radius: int, color: int, energy: fixed, di # a radial (omnidirectional) point light — the original short-signature call, # now a thin wrapper over light_emit with the cone disabled. -function light_point(cx: int, cy: int, radius: int, color: int, energy: fixed) -> void { - light_emit(cx, cy, radius, color, energy, 0, -1) +function light_point(rt_core_st: mut RtCoreState, rt_light_st: RtLightState, cx: int, cy: int, radius: int, color: int, energy: fixed) -> void { + light_emit(rt_core_st, rt_light_st, cx, cy, radius, color, energy, 0, -1) } # a cone / spot light aimed at `direction` degrees (0 = +x, CCW) with a half-angle # `spread` in degrees — a flashlight, a lamp cone. Shares every tier control (soft # shadows, falloff, gel, normals) with the radial form. -function light_spot(cx: int, cy: int, radius: int, color: int, energy: fixed, direction: int, spread: int) -> void { - light_emit(cx, cy, radius, color, energy, direction, spread) +function light_spot(rt_core_st: mut RtCoreState, rt_light_st: RtLightState, cx: int, cy: int, radius: int, color: int, energy: fixed, direction: int, spread: int) -> void { + light_emit(rt_core_st, rt_light_st, cx, cy, radius, color, energy, direction, spread) } # ---- day / night ---------------------------------------------------------- @@ -393,7 +395,7 @@ function light_spot(cx: int, cy: int, radius: int, color: int, energy: fixed, di # 0.25 dawn, 0.5 noon, 0.75 dusk). A cosine-free triangle ramps a deep-blue night # up to full daylight and back, so a game animates one value and the world's mood # follows. Deterministic; drives the same light_ambient modulate. -function light_time_of_day(t: fixed) -> void { +function light_time_of_day(rt_core_st: mut RtCoreState, t: fixed) -> void { # day factor 0..1: 0 at midnight, 1 at noon (triangle over the day). var day = t * fixed(2) # 0..2 across the day if day > fixed(1) { day = fixed(2) - day } # fold 0.5..1 back down: peak at noon @@ -404,5 +406,5 @@ function light_time_of_day(t: fixed) -> void { let r = nr + floor(fixed(dr - nr) * day) let g = ng + floor(fixed(dg - ng) * day) let b = nb + floor(fixed(db - nb) * day) - light_ambient((r << 16) | (g << 8) | b) + light_ambient(rt_core_st, (r << 16) | (g << 8) | b) } diff --git a/runtime/native/regex.ludic b/runtime/native/regex.ludic index 3b1ac555..ef64e99f 100644 --- a/runtime/native/regex.ludic +++ b/runtime/native/regex.ludic @@ -94,58 +94,61 @@ function rx_node(op: int) -> RNode { property Prog { code: IVec, cls: IVec, ngroups: int, ok: int } # ---- parser state ----------------------------------------------------------- -var rx_pat: pointer = "" -var rx_pos: int = 0 -var rx_len: int = 0 -var rx_err: int = 0 -var rx_ngroup: int = 0 -var rx_prog: Prog = null +export state RtRegexState { + rx_pat: pointer = "" + rx_pos: int = 0 + rx_len: int = 0 + rx_err: int = 0 + rx_ngroup: int = 0 + rx_prog: Prog = null + rx_named_gidx: int = 0 +} -function rx_peek() -> int { if rx_pos < rx_len { return rx_pat[rx_pos] & 255 }; return -1 } -function rx_peek2() -> int { if rx_pos + 1 < rx_len { return rx_pat[rx_pos + 1] & 255 }; return -1 } -function rx_adv() -> int { let c = rx_peek(); rx_pos += 1; return c } +function rx_peek(rt_regex_st: RtRegexState) -> int { if rt_regex_st.rx_pos < rt_regex_st.rx_len { return rt_regex_st.rx_pat[rt_regex_st.rx_pos] & 255 }; return -1 } +function rx_peek2(rt_regex_st: RtRegexState) -> int { if rt_regex_st.rx_pos + 1 < rt_regex_st.rx_len { return rt_regex_st.rx_pat[rt_regex_st.rx_pos + 1] & 255 }; return -1 } +function rx_adv(rt_regex_st: mut RtRegexState) -> int { let c = rx_peek(rt_regex_st); rt_regex_st.rx_pos += 1; return c } # ---- character classes ------------------------------------------------------ # a class is 8 i32 words (256 bits) in prog.cls; class k occupies cls[8k .. 8k+8) -function rx_class_new() -> int { - let idx = rx_prog.cls.n / 8 +function rx_class_new(rt_regex_st: RtRegexState) -> int { + let idx = rt_regex_st.rx_prog.cls.n / 8 var i = 0 - while i < 8 { iv_push(rx_prog.cls, 0); i += 1 } + while i < 8 { iv_push(rt_regex_st.rx_prog.cls, 0); i += 1 } return idx } -function rx_class_set(idx: int, c: int) -> void { +function rx_class_set(rt_regex_st: mut RtRegexState, idx: int, c: int) -> void { let w = idx * 8 + (c >> 5) - rx_prog.cls.d[w] = rx_prog.cls.d[w] | (1 << (c & 31)) + rt_regex_st.rx_prog.cls.d[w] = rt_regex_st.rx_prog.cls.d[w] | (1 << (c & 31)) } -function rx_class_set_range(idx: int, a: int, b: int) -> void { +function rx_class_set_range(rt_regex_st: mut RtRegexState, idx: int, a: int, b: int) -> void { var c = a - while c <= b { rx_class_set(idx, c); c += 1 } + while c <= b { rx_class_set(rt_regex_st, idx, c); c += 1 } } -function rx_class_negate(idx: int) -> void { +function rx_class_negate(rt_regex_st: mut RtRegexState, idx: int) -> void { var i = 0 - while i < 8 { let w = idx * 8 + i; rx_prog.cls.d[w] = ~rx_prog.cls.d[w]; i += 1 } + while i < 8 { let w = idx * 8 + i; rt_regex_st.rx_prog.cls.d[w] = ~rt_regex_st.rx_prog.cls.d[w]; i += 1 } } -function rx_class_unset(idx: int, c: int) -> void { +function rx_class_unset(rt_regex_st: mut RtRegexState, idx: int, c: int) -> void { let w = idx * 8 + (c >> 5) - rx_prog.cls.d[w] = rx_prog.cls.d[w] & ~(1 << (c & 31)) + rt_regex_st.rx_prog.cls.d[w] = rt_regex_st.rx_prog.cls.d[w] & ~(1 << (c & 31)) } -function rx_class_unset_range(idx: int, a: int, b: int) -> void { +function rx_class_unset_range(rt_regex_st: mut RtRegexState, idx: int, a: int, b: int) -> void { var c = a - while c <= b { rx_class_unset(idx, c); c += 1 } + while c <= b { rx_class_unset(rt_regex_st, idx, c); c += 1 } } -function rx_class_set_all(idx: int) -> void { +function rx_class_set_all(rt_regex_st: mut RtRegexState, idx: int) -> void { var i = 0 - while i < 8 { rx_prog.cls.d[idx * 8 + i] = -1; i += 1 } + while i < 8 { rt_regex_st.rx_prog.cls.d[idx * 8 + i] = -1; i += 1 } } -function rx_class_set_word(idx: int) -> void { - rx_class_set_range(idx, 48, 57) - rx_class_set_range(idx, 65, 90) - rx_class_set_range(idx, 97, 122) - rx_class_set(idx, 95) +function rx_class_set_word(rt_regex_st: mut RtRegexState, idx: int) -> void { + rx_class_set_range(rt_regex_st, idx, 48, 57) + rx_class_set_range(rt_regex_st, idx, 65, 90) + rx_class_set_range(rt_regex_st, idx, 97, 122) + rx_class_set(rt_regex_st, idx, 95) } -function rx_class_set_ws(idx: int) -> void { - rx_class_set(idx, 32); rx_class_set(idx, 9); rx_class_set(idx, 10) - rx_class_set(idx, 13); rx_class_set(idx, 12); rx_class_set(idx, 11) +function rx_class_set_ws(rt_regex_st: mut RtRegexState, idx: int) -> void { + rx_class_set(rt_regex_st, idx, 32); rx_class_set(rt_regex_st, idx, 9); rx_class_set(rt_regex_st, idx, 10) + rx_class_set(rt_regex_st, idx, 13); rx_class_set(rt_regex_st, idx, 12); rx_class_set(rt_regex_st, idx, 11) } function rx_class_has(prog: Prog, idx: int, c: int) -> bool { let w = prog.cls.d[idx * 8 + (c >> 5)] @@ -161,49 +164,49 @@ function rx_is_ws(c: int) -> bool { # OR the set named by \d \D \w \W \s \S into class idx. The negated forms OR in # the complement set bit-by-bit (never via set-all+unset, which would clobber a # previously-set member — e.g. the literal `1` in [1\Da] must survive \D). -function rx_class_add_pre(idx: int, kind: int) -> void { - if kind == 100 { rx_class_set_range(idx, 48, 57); return } # \d - if kind == 119 { rx_class_set_word(idx); return } # \w - if kind == 115 { rx_class_set_ws(idx); return } # \s +function rx_class_add_pre(rt_regex_st: mut RtRegexState, idx: int, kind: int) -> void { + if kind == 100 { rx_class_set_range(rt_regex_st, idx, 48, 57); return } # \d + if kind == 119 { rx_class_set_word(rt_regex_st, idx); return } # \w + if kind == 115 { rx_class_set_ws(rt_regex_st, idx); return } # \s var c = 0 while c < 256 { - if kind == 68 { if not rx_is_digit(c) { rx_class_set(idx, c) } } # \D - else if kind == 87 { if not rx_is_word(c) { rx_class_set(idx, c) } } # \W - else if kind == 83 { if not rx_is_ws(c) { rx_class_set(idx, c) } } # \S + if kind == 68 { if not rx_is_digit(c) { rx_class_set(rt_regex_st, idx, c) } } # \D + else if kind == 87 { if not rx_is_word(c) { rx_class_set(rt_regex_st, idx, c) } } # \W + else if kind == 83 { if not rx_is_ws(c) { rx_class_set(rt_regex_st, idx, c) } } # \S c += 1 } } # parse a [...] class starting at '['; returns an N_CLASS node -function rx_parse_class() -> RNode { - rx_adv() # consume '[' - let idx = rx_class_new() +function rx_parse_class(rt_regex_st: mut RtRegexState) -> RNode { + rx_adv(rt_regex_st) # consume '[' + let idx = rx_class_new(rt_regex_st) var neg = false - if rx_peek() == 94 { neg = true; rx_adv() } # [^ ...] + if rx_peek(rt_regex_st) == 94 { neg = true; rx_adv(rt_regex_st) } # [^ ...] # a ']' as the first char is a literal - if rx_peek() == 93 { rx_class_set(idx, 93); rx_adv() } - while rx_peek() != 93 and rx_peek() >= 0 { - var lo = rx_adv() + if rx_peek(rt_regex_st) == 93 { rx_class_set(rt_regex_st, idx, 93); rx_adv(rt_regex_st) } + while rx_peek(rt_regex_st) != 93 and rx_peek(rt_regex_st) >= 0 { + var lo = rx_adv(rt_regex_st) if lo == 92 { # escape inside class - let e = rx_adv() + let e = rx_adv(rt_regex_st) if e == 'd' or e == 'D' or e == 'w' or e == 'W' or e == 's' or e == 'S' { - rx_class_add_pre(idx, e) + rx_class_add_pre(rt_regex_st, idx, e) continue } lo = rx_class_escape_char(e) } # a range a-b (but a trailing '-' before ']' is literal) - if rx_peek() == 45 and rx_peek2() != 93 and rx_peek2() >= 0 { - rx_adv() # consume '-' - var hi = rx_adv() - if hi == 92 { hi = rx_class_escape_char(rx_adv()) } - rx_class_set_range(idx, lo, hi) + if rx_peek(rt_regex_st) == 45 and rx_peek2(rt_regex_st) != 93 and rx_peek2(rt_regex_st) >= 0 { + rx_adv(rt_regex_st) # consume '-' + var hi = rx_adv(rt_regex_st) + if hi == 92 { hi = rx_class_escape_char(rx_adv(rt_regex_st)) } + rx_class_set_range(rt_regex_st, idx, lo, hi) } else { - rx_class_set(idx, lo) + rx_class_set(rt_regex_st, idx, lo) } } - if rx_peek() != 93 { rx_err = 1 } else { rx_adv() } # consume ']' - if neg { rx_class_negate(idx) } + if rx_peek(rt_regex_st) != 93 { rt_regex_st.rx_err = 1 } else { rx_adv(rt_regex_st) } # consume ']' + if neg { rx_class_negate(rt_regex_st, idx) } let node = rx_node(N_CLASS) node.cls = idx return node @@ -220,17 +223,17 @@ function rx_class_escape_char(e: int) -> int { } # ---- escapes outside a class ------------------------------------------------ -function rx_parse_escape() -> RNode { - rx_adv() # consume '\' - let e = rx_adv() +function rx_parse_escape(rt_regex_st: mut RtRegexState) -> RNode { + rx_adv(rt_regex_st) # consume '\' + let e = rx_adv(rt_regex_st) if e == 'd' or e == 'D' or e == 'w' or e == 'W' or e == 's' or e == 'S' { - let idx = rx_class_new() - rx_class_add_pre(idx, e) + let idx = rx_class_new(rt_regex_st) + rx_class_add_pre(rt_regex_st, idx, e) let node = rx_node(N_CLASS) node.cls = idx return node } - if e >= '1' and e <= '9' { rx_err = 1; return rx_node(N_EMPTY) } # backrefs unsupported + if e >= '1' and e <= '9' { rt_regex_st.rx_err = 1; return rx_node(N_EMPTY) } # backrefs unsupported var ch = e if e == 'n' { ch = 10 } else if e == 't' { ch = 9 } @@ -238,165 +241,164 @@ function rx_parse_escape() -> RNode { else if e == 'f' { ch = 12 } else if e == 'v' { ch = 11 } else if e == '0' { ch = 0 } - else if e < 0 { rx_err = 1; return rx_node(N_EMPTY) } + else if e < 0 { rt_regex_st.rx_err = 1; return rx_node(N_EMPTY) } let node = rx_node(N_LIT) node.ch = ch return node } # ---- parser ----------------------------------------------------------------- -function rx_parse_alt() -> RNode { - let first = rx_parse_concat() - if rx_peek() != 124 { return first } +function rx_parse_alt(rt_regex_st: mut RtRegexState) -> RNode { + let first = rx_parse_concat(rt_regex_st) + if rx_peek(rt_regex_st) != 124 { return first } let alt = rx_node(N_ALT) push(alt.kids, first) - while rx_peek() == 124 { - rx_adv() - push(alt.kids, rx_parse_concat()) - if rx_err == 1 { break } + while rx_peek(rt_regex_st) == 124 { + rx_adv(rt_regex_st) + push(alt.kids, rx_parse_concat(rt_regex_st)) + if rt_regex_st.rx_err == 1 { break } } return alt } -function rx_parse_concat() -> RNode { +function rx_parse_concat(rt_regex_st: mut RtRegexState) -> RNode { let cat = rx_node(N_CONCAT) while true { - let c = rx_peek() + let c = rx_peek(rt_regex_st) if c < 0 or c == '|' or c == ')' { break } - push(cat.kids, rx_parse_repeat()) - if rx_err == 1 { break } + push(cat.kids, rx_parse_repeat(rt_regex_st)) + if rt_regex_st.rx_err == 1 { break } } if len(cat.kids) == 1 { return cat.kids[0] } if len(cat.kids) == 0 { return rx_node(N_EMPTY) } return cat } # read the optional lazy '?' after a quantifier; returns greedy flag (0 = lazy) -function rx_lazy() -> int { - if rx_peek() == 63 { rx_adv(); return 0 } +function rx_lazy(rt_regex_st: mut RtRegexState) -> int { + if rx_peek(rt_regex_st) == 63 { rx_adv(rt_regex_st); return 0 } return 1 } -function rx_parse_repeat() -> RNode { - let atom = rx_parse_atom() - if rx_err == 1 { return atom } - let c = rx_peek() +function rx_parse_repeat(rt_regex_st: mut RtRegexState) -> RNode { + let atom = rx_parse_atom(rt_regex_st) + if rt_regex_st.rx_err == 1 { return atom } + let c = rx_peek(rt_regex_st) if c == '*' or c == '+' or c == '?' { - rx_adv() + rx_adv(rt_regex_st) var op = N_STAR if c == '+' { op = N_PLUS } if c == '?' { op = N_QUEST } let r = rx_node(op) - r.greedy = rx_lazy() + r.greedy = rx_lazy(rt_regex_st) push(r.kids, atom) return r } - if c == '{' { return rx_parse_brace(atom) } + if c == '{' { return rx_parse_brace(rt_regex_st, atom) } return atom } # {n} {n,} {n,m} -function rx_parse_brace(atom: RNode) -> RNode { - let save = rx_pos - rx_adv() # consume '{' +function rx_parse_brace(rt_regex_st: mut RtRegexState, atom: RNode) -> RNode { + let save = rt_regex_st.rx_pos + rx_adv(rt_regex_st) # consume '{' var lo = 0 var haslo = false - while rx_peek() >= 48 and rx_peek() <= 57 { lo = lo * 10 + (rx_adv() - 48); haslo = true } + while rx_peek(rt_regex_st) >= 48 and rx_peek(rt_regex_st) <= 57 { lo = lo * 10 + (rx_adv(rt_regex_st) - 48); haslo = true } var hi = lo var hasComma = false - if rx_peek() == 44 { hasComma = true; rx_adv(); hi = -1 + if rx_peek(rt_regex_st) == 44 { hasComma = true; rx_adv(rt_regex_st); hi = -1 var hashi = false - while rx_peek() >= 48 and rx_peek() <= 57 { if not hashi { hi = 0 }; hi = hi * 10 + (rx_adv() - 48); hashi = true } + while rx_peek(rt_regex_st) >= 48 and rx_peek(rt_regex_st) <= 57 { if not hashi { hi = 0 }; hi = hi * 10 + (rx_adv(rt_regex_st) - 48); hashi = true } } - if rx_peek() != 125 or not haslo { # not a valid brace -> literal '{' - rx_pos = save - rx_adv() + if rx_peek(rt_regex_st) != 125 or not haslo { # not a valid brace -> literal '{' + rt_regex_st.rx_pos = save + rx_adv(rt_regex_st) let n = rx_node(N_LIT); n.ch = 123; return n } - rx_adv() # consume '}' + rx_adv(rt_regex_st) # consume '}' let r = rx_node(N_REP) r.lo = lo r.hi = hi - r.greedy = rx_lazy() + r.greedy = rx_lazy(rt_regex_st) push(r.kids, atom) return r } -function rx_parse_atom() -> RNode { - let c = rx_peek() +function rx_parse_atom(rt_regex_st: mut RtRegexState) -> RNode { + let c = rx_peek(rt_regex_st) if c == '(' { # '(' - rx_adv() + rx_adv(rt_regex_st) var gidx = -1 - if rx_peek() == 63 { # (? ... - rx_adv() - let d = rx_peek() - if d == ':' { rx_adv() } # (?: non-capturing - else if d == 'P' { rx_adv(); rx_skip_name() } # (?P (captured, name ignored for now) - else if d == '<' { rx_skip_name() } # (? - else { rx_err = 1; return rx_node(N_EMPTY) } + if rx_peek(rt_regex_st) == 63 { # (? ... + rx_adv(rt_regex_st) + let d = rx_peek(rt_regex_st) + if d == ':' { rx_adv(rt_regex_st) } # (?: non-capturing + else if d == 'P' { rx_adv(rt_regex_st); rx_skip_name(rt_regex_st) } # (?P (captured, name ignored for now) + else if d == '<' { rx_skip_name(rt_regex_st) } # (? + else { rt_regex_st.rx_err = 1; return rx_node(N_EMPTY) } } else { - rx_ngroup += 1 - gidx = rx_ngroup + rt_regex_st.rx_ngroup += 1 + gidx = rt_regex_st.rx_ngroup } - let inner = rx_parse_alt() - if rx_peek() != 41 { rx_err = 1; return rx_node(N_EMPTY) } - rx_adv() # consume ')' + let inner = rx_parse_alt(rt_regex_st) + if rx_peek(rt_regex_st) != 41 { rt_regex_st.rx_err = 1; return rx_node(N_EMPTY) } + rx_adv(rt_regex_st) # consume ')' let g = rx_node(N_GROUP) g.gidx = gidx push(g.kids, inner) return g } - if c == '[' { return rx_parse_class() } - if c == '.' { rx_adv(); return rx_node(N_ANY) } - if c == '^' { rx_adv(); return rx_node(N_BOL) } - if c == '$' { rx_adv(); return rx_node(N_EOL) } - if c == '\\' { return rx_parse_escape() } - if c == '*' or c == '+' or c == '?' or c == ')' { rx_err = 1; return rx_node(N_EMPTY) } - if c < 0 { rx_err = 1; return rx_node(N_EMPTY) } - rx_adv() + if c == '[' { return rx_parse_class(rt_regex_st) } + if c == '.' { rx_adv(rt_regex_st); return rx_node(N_ANY) } + if c == '^' { rx_adv(rt_regex_st); return rx_node(N_BOL) } + if c == '$' { rx_adv(rt_regex_st); return rx_node(N_EOL) } + if c == '\\' { return rx_parse_escape(rt_regex_st) } + if c == '*' or c == '+' or c == '?' or c == ')' { rt_regex_st.rx_err = 1; return rx_node(N_EMPTY) } + if c < 0 { rt_regex_st.rx_err = 1; return rx_node(N_EMPTY) } + rx_adv(rt_regex_st) let n = rx_node(N_LIT) n.ch = c return n } # skip a (?P / (? group name up to '>'; leaves a capturing group -function rx_skip_name() -> void { - if rx_peek() == 80 { rx_adv() } # already consumed by caller in (?P case? guard - if rx_peek() == 60 { rx_adv() } # consume '<' - while rx_peek() != 62 and rx_peek() >= 0 { rx_adv() } - if rx_peek() == 62 { rx_adv() } # consume '>' - rx_ngroup += 1 +function rx_skip_name(rt_regex_st: mut RtRegexState) -> void { + if rx_peek(rt_regex_st) == 80 { rx_adv(rt_regex_st) } # already consumed by caller in (?P case? guard + if rx_peek(rt_regex_st) == 60 { rx_adv(rt_regex_st) } # consume '<' + while rx_peek(rt_regex_st) != 62 and rx_peek(rt_regex_st) >= 0 { rx_adv(rt_regex_st) } + if rx_peek(rt_regex_st) == 62 { rx_adv(rt_regex_st) } # consume '>' + rt_regex_st.rx_ngroup += 1 # note: the caller set gidx = -1; fix it up to a real capture index - rx_named_gidx = rx_ngroup + rt_regex_st.rx_named_gidx = rt_regex_st.rx_ngroup } -var rx_named_gidx: int = 0 # ---- compile AST -> program ------------------------------------------------- -function pg_emit(op: int, a: int, b: int) -> int { - let pc = rx_prog.code.n / 3 - iv_push(rx_prog.code, op) - iv_push(rx_prog.code, a) - iv_push(rx_prog.code, b) +function pg_emit(rt_regex_st: RtRegexState, op: int, a: int, b: int) -> int { + let pc = rt_regex_st.rx_prog.code.n / 3 + iv_push(rt_regex_st.rx_prog.code, op) + iv_push(rt_regex_st.rx_prog.code, a) + iv_push(rt_regex_st.rx_prog.code, b) return pc } -function pg_set_a(pc: int, a: int) -> void { rx_prog.code.d[3 * pc + 1] = a } -function pg_set_b(pc: int, b: int) -> void { rx_prog.code.d[3 * pc + 2] = b } -function pg_pc() -> int { return rx_prog.code.n / 3 } +function pg_set_a(rt_regex_st: mut RtRegexState, pc: int, a: int) -> void { rt_regex_st.rx_prog.code.d[3 * pc + 1] = a } +function pg_set_b(rt_regex_st: mut RtRegexState, pc: int, b: int) -> void { rt_regex_st.rx_prog.code.d[3 * pc + 2] = b } +function pg_pc(rt_regex_st: RtRegexState) -> int { return rt_regex_st.rx_prog.code.n / 3 } -function rx_compile(node: RNode) -> void { +function rx_compile(rt_regex_st: mut RtRegexState, node: RNode) -> void { let op = node.op if op == N_EMPTY { return } - if op == N_LIT { pg_emit(OP_CHAR, node.ch, 0); return } - if op == N_ANY { pg_emit(OP_ANY, 0, 0); return } - if op == N_CLASS { pg_emit(OP_CLASS, node.cls, 0); return } - if op == N_BOL { pg_emit(OP_BOL, 0, 0); return } - if op == N_EOL { pg_emit(OP_EOL, 0, 0); return } + if op == N_LIT { pg_emit(rt_regex_st, OP_CHAR, node.ch, 0); return } + if op == N_ANY { pg_emit(rt_regex_st, OP_ANY, 0, 0); return } + if op == N_CLASS { pg_emit(rt_regex_st, OP_CLASS, node.cls, 0); return } + if op == N_BOL { pg_emit(rt_regex_st, OP_BOL, 0, 0); return } + if op == N_EOL { pg_emit(rt_regex_st, OP_EOL, 0, 0); return } if op == N_CONCAT { var i = 0 - while i < len(node.kids) { rx_compile(node.kids[i]); i += 1 } + while i < len(node.kids) { rx_compile(rt_regex_st, node.kids[i]); i += 1 } return } if op == N_GROUP { if node.gidx >= 0 { - pg_emit(OP_SAVE, 2 * node.gidx, 0) - rx_compile(node.kids[0]) - pg_emit(OP_SAVE, 2 * node.gidx + 1, 0) + pg_emit(rt_regex_st, OP_SAVE, 2 * node.gidx, 0) + rx_compile(rt_regex_st, node.kids[0]) + pg_emit(rt_regex_st, OP_SAVE, 2 * node.gidx + 1, 0) } else { - rx_compile(node.kids[0]) + rx_compile(rt_regex_st, node.kids[0]) } return } @@ -405,67 +407,67 @@ function rx_compile(node: RNode) -> void { var i = 0 while i < len(node.kids) { if i < len(node.kids) - 1 { - let sp = pg_emit(OP_SPLIT, 0, 0) - pg_set_a(sp, pg_pc()) - rx_compile(node.kids[i]) - let j = pg_emit(OP_JMP, 0, 0) + let sp = pg_emit(rt_regex_st, OP_SPLIT, 0, 0) + pg_set_a(rt_regex_st, sp, pg_pc(rt_regex_st)) + rx_compile(rt_regex_st, node.kids[i]) + let j = pg_emit(rt_regex_st, OP_JMP, 0, 0) iv_push(jmps, j) - pg_set_b(sp, pg_pc()) + pg_set_b(rt_regex_st, sp, pg_pc(rt_regex_st)) } else { - rx_compile(node.kids[i]) + rx_compile(rt_regex_st, node.kids[i]) } i += 1 } - let end = pg_pc() + let end = pg_pc(rt_regex_st) i = 0 - while i < jmps.n { pg_set_a(jmps.d[i], end); i += 1 } + while i < jmps.n { pg_set_a(rt_regex_st, jmps.d[i], end); i += 1 } return } if op == N_STAR { - let l1 = pg_pc() - let sp = pg_emit(OP_SPLIT, 0, 0) - let l2 = pg_pc() - rx_compile(node.kids[0]) - pg_emit(OP_JMP, l1, 0) - let l3 = pg_pc() - if node.greedy == 1 { pg_set_a(sp, l2); pg_set_b(sp, l3) } - else { pg_set_a(sp, l3); pg_set_b(sp, l2) } + let l1 = pg_pc(rt_regex_st) + let sp = pg_emit(rt_regex_st, OP_SPLIT, 0, 0) + let l2 = pg_pc(rt_regex_st) + rx_compile(rt_regex_st, node.kids[0]) + pg_emit(rt_regex_st, OP_JMP, l1, 0) + let l3 = pg_pc(rt_regex_st) + if node.greedy == 1 { pg_set_a(rt_regex_st, sp, l2); pg_set_b(rt_regex_st, sp, l3) } + else { pg_set_a(rt_regex_st, sp, l3); pg_set_b(rt_regex_st, sp, l2) } return } if op == N_PLUS { - let l1 = pg_pc() - rx_compile(node.kids[0]) - let sp = pg_emit(OP_SPLIT, 0, 0) - let l3 = pg_pc() - if node.greedy == 1 { pg_set_a(sp, l1); pg_set_b(sp, l3) } - else { pg_set_a(sp, l3); pg_set_b(sp, l1) } + let l1 = pg_pc(rt_regex_st) + rx_compile(rt_regex_st, node.kids[0]) + let sp = pg_emit(rt_regex_st, OP_SPLIT, 0, 0) + let l3 = pg_pc(rt_regex_st) + if node.greedy == 1 { pg_set_a(rt_regex_st, sp, l1); pg_set_b(rt_regex_st, sp, l3) } + else { pg_set_a(rt_regex_st, sp, l3); pg_set_b(rt_regex_st, sp, l1) } return } if op == N_QUEST { - let sp = pg_emit(OP_SPLIT, 0, 0) - let l2 = pg_pc() - rx_compile(node.kids[0]) - let l3 = pg_pc() - if node.greedy == 1 { pg_set_a(sp, l2); pg_set_b(sp, l3) } - else { pg_set_a(sp, l3); pg_set_b(sp, l2) } + let sp = pg_emit(rt_regex_st, OP_SPLIT, 0, 0) + let l2 = pg_pc(rt_regex_st) + rx_compile(rt_regex_st, node.kids[0]) + let l3 = pg_pc(rt_regex_st) + if node.greedy == 1 { pg_set_a(rt_regex_st, sp, l2); pg_set_b(rt_regex_st, sp, l3) } + else { pg_set_a(rt_regex_st, sp, l3); pg_set_b(rt_regex_st, sp, l2) } return } if op == N_REP { let kid = node.kids[0] var i = 0 - while i < node.lo { rx_compile(kid); i += 1 } + while i < node.lo { rx_compile(rt_regex_st, kid); i += 1 } if node.hi < 0 { let st = rx_node(N_STAR) st.greedy = node.greedy push(st.kids, kid) - rx_compile(st) + rx_compile(rt_regex_st, st) } else { i = 0 while i < node.hi - node.lo { let q = rx_node(N_QUEST) q.greedy = node.greedy push(q.kids, kid) - rx_compile(q) + rx_compile(rt_regex_st, q) i += 1 } } @@ -474,32 +476,32 @@ function rx_compile(node: RNode) -> void { } # regex_compile(pattern) -> Prog (null on a syntax error) -function regex_compile(pattern: pointer) -> Prog { +function regex_compile(rt_regex_st: mut RtRegexState, pattern: pointer) -> Prog { let p = new Prog p.code = iv_new() p.cls = iv_new() p.ngroups = 0 p.ok = 1 - rx_prog = p - rx_pat = pattern - rx_pos = 0 - rx_len = rx_slen(pattern) - rx_err = 0 - rx_ngroup = 0 - let root = rx_parse_alt() - if rx_err == 1 or rx_pos != rx_len { return null } - p.ngroups = rx_ngroup + rt_regex_st.rx_prog = p + rt_regex_st.rx_pat = pattern + rt_regex_st.rx_pos = 0 + rt_regex_st.rx_len = rx_slen(pattern) + rt_regex_st.rx_err = 0 + rt_regex_st.rx_ngroup = 0 + let root = rx_parse_alt(rt_regex_st) + if rt_regex_st.rx_err == 1 or rt_regex_st.rx_pos != rt_regex_st.rx_len { return null } + p.ngroups = rt_regex_st.rx_ngroup # unanchored lazy .*? prefix so a match may start at any position - let sp0 = pg_emit(OP_SPLIT, 0, 0) - let consume = pg_pc() - pg_emit(OP_ANYNL, 0, 0) - pg_emit(OP_JMP, sp0, 0) - let body = pg_pc() - pg_set_a(sp0, body) - pg_set_b(sp0, consume) - pg_emit(OP_SAVE, 0, 0) - rx_compile(root) - pg_emit(OP_SAVE, 1, 0) - pg_emit(OP_MATCH, 0, 0) + let sp0 = pg_emit(rt_regex_st, OP_SPLIT, 0, 0) + let consume = pg_pc(rt_regex_st) + pg_emit(rt_regex_st, OP_ANYNL, 0, 0) + pg_emit(rt_regex_st, OP_JMP, sp0, 0) + let body = pg_pc(rt_regex_st) + pg_set_a(rt_regex_st, sp0, body) + pg_set_b(rt_regex_st, sp0, consume) + pg_emit(rt_regex_st, OP_SAVE, 0, 0) + rx_compile(rt_regex_st, root) + pg_emit(rt_regex_st, OP_SAVE, 1, 0) + pg_emit(rt_regex_st, OP_MATCH, 0, 0) return p } diff --git a/runtime/native/regex_vm.ludic b/runtime/native/regex_vm.ludic index 2cfb63aa..6944e867 100644 --- a/runtime/native/regex_vm.ludic +++ b/runtime/native/regex_vm.ludic @@ -7,66 +7,68 @@ # ---- Pike VM ---------------------------------------------------------------- property TList { pc: words, caps: words, n: int } -var rx_seen: words = null -var rx_gen: int = 0 -var rx_nc: int = 0 -var rx_code: words = null +export state RtRegexVmState { + rx_seen: words = null + rx_gen: int = 0 + rx_nc: int = 0 + rx_code: words = null +} -function rx_add(list: TList, pc: int, caps: words, sp: int, s: pointer, slen: int) -> void { - if rx_seen[pc] == rx_gen { return } - rx_seen[pc] = rx_gen - let op = rx_code[3 * pc] - if op == OP_JMP { rx_add(list, rx_code[3 * pc + 1], caps, sp, s, slen); return } +function rx_add(rt_regex_vm_st: mut RtRegexVmState, list: TList, pc: int, caps: words, sp: int, s: pointer, slen: int) -> void { + if rt_regex_vm_st.rx_seen[pc] == rt_regex_vm_st.rx_gen { return } + rt_regex_vm_st.rx_seen[pc] = rt_regex_vm_st.rx_gen + let op = rt_regex_vm_st.rx_code[3 * pc] + if op == OP_JMP { rx_add(rt_regex_vm_st, list, rt_regex_vm_st.rx_code[3 * pc + 1], caps, sp, s, slen); return } if op == OP_SPLIT { - rx_add(list, rx_code[3 * pc + 1], caps, sp, s, slen) - rx_add(list, rx_code[3 * pc + 2], caps, sp, s, slen) + rx_add(rt_regex_vm_st, list, rt_regex_vm_st.rx_code[3 * pc + 1], caps, sp, s, slen) + rx_add(rt_regex_vm_st, list, rt_regex_vm_st.rx_code[3 * pc + 2], caps, sp, s, slen) return } if op == OP_SAVE { - let slot = rx_code[3 * pc + 1] + let slot = rt_regex_vm_st.rx_code[3 * pc + 1] let old = caps[slot] caps[slot] = sp - rx_add(list, pc + 1, caps, sp, s, slen) + rx_add(rt_regex_vm_st, list, pc + 1, caps, sp, s, slen) caps[slot] = old return } if op == OP_BOL { - if sp == 0 { rx_add(list, pc + 1, caps, sp, s, slen) } + if sp == 0 { rx_add(rt_regex_vm_st, list, pc + 1, caps, sp, s, slen) } return } if op == OP_EOL { - if sp == slen { rx_add(list, pc + 1, caps, sp, s, slen) } - else if sp == slen - 1 and (s[sp] & 255) == 10 { rx_add(list, pc + 1, caps, sp, s, slen) } + if sp == slen { rx_add(rt_regex_vm_st, list, pc + 1, caps, sp, s, slen) } + else if sp == slen - 1 and (s[sp] & 255) == 10 { rx_add(rt_regex_vm_st, list, pc + 1, caps, sp, s, slen) } return } # a leaf that consumes (CHAR/ANY/ANYNL/CLASS) or MATCH: record it let t = list.n list.pc[t] = pc var i = 0 - while i < rx_nc { list.caps[t * rx_nc + i] = caps[i]; i += 1 } + while i < rt_regex_vm_st.rx_nc { list.caps[t * rt_regex_vm_st.rx_nc + i] = caps[i]; i += 1 } list.n += 1 } # run prog over s (length slen) from startpos; returns caps words or null -function rx_run(prog: Prog, s: pointer, slen: int, startpos: int) -> words { - rx_code = prog.code.d - rx_nc = 2 * (prog.ngroups + 1) +function rx_run(rt_regex_vm_st: mut RtRegexVmState, prog: Prog, s: pointer, slen: int, startpos: int) -> words { + rt_regex_vm_st.rx_code = prog.code.d + rt_regex_vm_st.rx_nc = 2 * (prog.ngroups + 1) let ncode = prog.code.n / 3 - rx_seen = words(ncode) + rt_regex_vm_st.rx_seen = words(ncode) var i = 0 - while i < ncode { rx_seen[i] = 0; i += 1 } - rx_gen = 0 + while i < ncode { rt_regex_vm_st.rx_seen[i] = 0; i += 1 } + rt_regex_vm_st.rx_gen = 0 var clist = new TList - clist.pc = words(ncode); clist.caps = words(ncode * rx_nc); clist.n = 0 + clist.pc = words(ncode); clist.caps = words(ncode * rt_regex_vm_st.rx_nc); clist.n = 0 var nlist = new TList - nlist.pc = words(ncode); nlist.caps = words(ncode * rx_nc); nlist.n = 0 - let wcaps = words(rx_nc) + nlist.pc = words(ncode); nlist.caps = words(ncode * rt_regex_vm_st.rx_nc); nlist.n = 0 + let wcaps = words(rt_regex_vm_st.rx_nc) var matched: words = null - rx_gen += 1 + rt_regex_vm_st.rx_gen += 1 i = 0 - while i < rx_nc { wcaps[i] = -1; i += 1 } - rx_add(clist, 0, wcaps, startpos, s, slen) + while i < rt_regex_vm_st.rx_nc { wcaps[i] = -1; i += 1 } + rx_add(rt_regex_vm_st, clist, 0, wcaps, startpos, s, slen) var sp = startpos while true { @@ -74,26 +76,26 @@ function rx_run(prog: Prog, s: pointer, slen: int, startpos: int) -> words { var c = -1 if sp < slen { c = s[sp] & 255 } nlist.n = 0 - rx_gen += 1 + rt_regex_vm_st.rx_gen += 1 var ti = 0 var stop = false while ti < clist.n and not stop { let pc = clist.pc[ti] var k = 0 - while k < rx_nc { wcaps[k] = clist.caps[ti * rx_nc + k]; k += 1 } - let op = rx_code[3 * pc] + while k < rt_regex_vm_st.rx_nc { wcaps[k] = clist.caps[ti * rt_regex_vm_st.rx_nc + k]; k += 1 } + let op = rt_regex_vm_st.rx_code[3 * pc] if op == OP_CHAR { - if c >= 0 and c == rx_code[3 * pc + 1] { rx_add(nlist, pc + 1, wcaps, sp + 1, s, slen) } + if c >= 0 and c == rt_regex_vm_st.rx_code[3 * pc + 1] { rx_add(rt_regex_vm_st, nlist, pc + 1, wcaps, sp + 1, s, slen) } } else if op == OP_ANY { - if c >= 0 and c != '\n' { rx_add(nlist, pc + 1, wcaps, sp + 1, s, slen) } + if c >= 0 and c != '\n' { rx_add(rt_regex_vm_st, nlist, pc + 1, wcaps, sp + 1, s, slen) } } else if op == OP_ANYNL { - if c >= 0 { rx_add(nlist, pc + 1, wcaps, sp + 1, s, slen) } + if c >= 0 { rx_add(rt_regex_vm_st, nlist, pc + 1, wcaps, sp + 1, s, slen) } } else if op == OP_CLASS { - if c >= 0 and rx_class_has(prog, rx_code[3 * pc + 1], c) { rx_add(nlist, pc + 1, wcaps, sp + 1, s, slen) } + if c >= 0 and rx_class_has(prog, rt_regex_vm_st.rx_code[3 * pc + 1], c) { rx_add(rt_regex_vm_st, nlist, pc + 1, wcaps, sp + 1, s, slen) } } else if op == OP_MATCH { - if matched == null { matched = words(rx_nc) } + if matched == null { matched = words(rt_regex_vm_st.rx_nc) } k = 0 - while k < rx_nc { matched[k] = wcaps[k]; k += 1 } + while k < rt_regex_vm_st.rx_nc { matched[k] = wcaps[k]; k += 1 } stop = true } ti += 1 @@ -108,33 +110,33 @@ function rx_run(prog: Prog, s: pointer, slen: int, startpos: int) -> words { # ---- public API ------------------------------------------------------------- property Match { str: pointer = null, ng: int = 0, caps: words = null } -function regex_matches(str: pointer, pattern: pointer) -> bool { - let p = regex_compile(pattern) +function regex_matches(rt_regex_st: mut RtRegexState, rt_regex_vm_st: mut RtRegexVmState, str: pointer, pattern: pointer) -> bool { + let p = regex_compile(rt_regex_st, pattern) if p == null { return false } - return rx_run(p, str, rx_slen(str), 0) != null + return rx_run(rt_regex_vm_st, p, str, rx_slen(str), 0) != null } -function regex_test(str: pointer, re: Prog) -> bool { +function regex_test(rt_regex_vm_st: mut RtRegexVmState, str: pointer, re: Prog) -> bool { if re == null { return false } - return rx_run(re, str, rx_slen(str), 0) != null + return rx_run(rt_regex_vm_st, re, str, rx_slen(str), 0) != null } -function regex_exec(str: pointer, re: Prog) -> Match { +function regex_exec(rt_regex_vm_st: mut RtRegexVmState, str: pointer, re: Prog) -> Match { if re == null { return null } - let caps = rx_run(re, str, rx_slen(str), 0) + let caps = rx_run(rt_regex_vm_st, re, str, rx_slen(str), 0) if caps == null { return null } let m = new Match m.str = str; m.ng = re.ngroups; m.caps = caps return m } -function regex_find(str: pointer, pattern: pointer) -> Match { - let p = regex_compile(pattern) +function regex_find(rt_regex_st: mut RtRegexState, rt_regex_vm_st: mut RtRegexVmState, str: pointer, pattern: pointer) -> Match { + let p = regex_compile(rt_regex_st, pattern) if p == null { return null } - return regex_exec(str, p) + return regex_exec(rt_regex_vm_st, str, p) } -function regex_next(str: pointer, re: Prog, from: int) -> Match { +function regex_next(rt_regex_vm_st: mut RtRegexVmState, str: pointer, re: Prog, from: int) -> Match { if re == null { return null } let n = rx_slen(str) if from > n { return null } - let caps = rx_run(re, str, n, from) + let caps = rx_run(rt_regex_vm_st, re, str, n, from) if caps == null { return null } let m = new Match m.str = str; m.ng = re.ngroups; m.caps = caps @@ -161,7 +163,7 @@ function regex_group(m: Match, n: int) -> string { out[b - a] = 0 return out } -function regex_valid(pattern: pointer) -> bool { return regex_compile(pattern) != null } +function regex_valid(rt_regex_st: mut RtRegexState, pattern: pointer) -> bool { return regex_compile(rt_regex_st, pattern) != null } # regex_replace(str, pattern, repl): replace all non-overlapping matches. # repl expands \0..\9 (groups; \0 = whole match) and \\ (a literal backslash). @@ -169,8 +171,8 @@ function rx_append(out: IVec, s: pointer, a: int, b: int) -> void { var i = a while i < b { iv_push(out, s[i] & 255); i += 1 } } -function regex_replace(str: pointer, pattern: pointer, repl: pointer) -> string { - let p = regex_compile(pattern) +function regex_replace(rt_regex_st: mut RtRegexState, rt_regex_vm_st: mut RtRegexVmState, str: pointer, pattern: pointer, repl: pointer) -> string { + let p = regex_compile(rt_regex_st, pattern) if p == null { return str } let n = rx_slen(str) let rn = rx_slen(repl) @@ -178,7 +180,7 @@ function regex_replace(str: pointer, pattern: pointer, repl: pointer) -> string var pos = 0 var prev = 0 while pos <= n { - let caps = rx_run(p, str, n, pos) + let caps = rx_run(rt_regex_vm_st, p, str, n, pos) if caps == null { break } let ms = caps[0] let me = caps[1] diff --git a/runtime/native/rng.ludic b/runtime/native/rng.ludic index 47c8039c..18c7894c 100644 --- a/runtime/native/rng.ludic +++ b/runtime/native/rng.ludic @@ -1,63 +1,65 @@ # rng.ludic — the engine's seeded random numbers (Random.*, seed, rng_*): xorshift32, the same # stream on every machine. Its own file so a program with no ECS - a tool, a test program - gets # Random.* too: the compiler splices it on demand, and core.ludic imports it for a game. -var rt_rng: int = 305419896 # xorshift32 state +export state RtRngState { + rt_rng: int = 305419896 # xorshift32 state +} # ---- rng (xorshift32) ----------------------------------------------------- -function rt_seed(s: int) -> void { +function rt_seed(rt_rng_st: mut RtRngState, s: int) -> void { if s == 0 { - rt_rng = 305419896 + rt_rng_st.rt_rng = 305419896 return } - rt_rng = s + rt_rng_st.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. -function rt_next_rand() -> int { - var x = rt_rng +function rt_next_rand(rt_rng_st: mut RtRngState) -> int { + var x = rt_rng_st.rt_rng x = (x ^ (x << 13)) x = (x ^ (x >> 17)) x = (x ^ (x << 5)) - rt_rng = x + rt_rng_st.rt_rng = x return (x & 2147483647) } -function rt_rng_range(lo: int, hi: int) -> int { +function rt_rng_range(rt_rng_st: mut RtRngState, lo: int, hi: int) -> int { if hi <= lo { return lo } - return lo + rt_next_rand() % (hi - lo + 1) + return lo + rt_next_rand(rt_rng_st) % (hi - lo + 1) } -function rt_rng_chance(pct: int) -> bool { - return rt_next_rand() % 100 < pct +function rt_rng_chance(rt_rng_st: mut RtRngState, pct: int) -> bool { + return rt_next_rand(rt_rng_st) % 100 < pct } # a deterministic fixed-point value in [0, 1) — the raw 0..65535 is exactly the # Q16.16 fraction (fixed and int share the i32 representation). -function rt_rng_value() -> fixed { - return rt_rng_range(0, 65535) +function rt_rng_value(rt_rng_st: mut RtRngState) -> fixed { + return rt_rng_range(rt_rng_st, 0, 65535) } # a deterministic integer in [0, max) — 0 when max <= 0 -function rt_rng_int(max: int) -> int { +function rt_rng_int(rt_rng_st: mut RtRngState, max: int) -> int { if max <= 0 { return 0 } - return rt_rng_range(0, max - 1) + return rt_rng_range(rt_rng_st, 0, max - 1) } # a deterministic +1 or -1 -function rt_rng_sign() -> int { - if rt_rng_chance(50) { return 1 } +function rt_rng_sign(rt_rng_st: mut RtRngState) -> int { + if rt_rng_chance(rt_rng_st, 50) { return 1 } return -1 } # Random.weighted(weights): an index drawn in proportion to its weight (0 = never); # -1 when every weight is 0. Deterministic, from the seeded RNG. -function rt_rng_weighted(weights: []int) -> int { +function rt_rng_weighted(rt_rng_st: mut RtRngState, weights: []int) -> int { var total = 0 var i = 0 while i < len(weights) { if weights[i] > 0 { total += weights[i] }; i += 1 } if total <= 0 { return -1 } - var roll = rt_rng_range(0, total - 1) + var roll = rt_rng_range(rt_rng_st, 0, total - 1) i = 0 while i < len(weights) { if weights[i] > 0 { diff --git a/runtime/native/systems.ludic b/runtime/native/systems.ludic index 709646af..4b65a1ce 100644 --- a/runtime/native/systems.ludic +++ b/runtime/native/systems.ludic @@ -35,47 +35,49 @@ function esys_div(a: int, b: int) -> int { # same shape the input action map uses; string names compare by byte-pointer # identity (a string literal interns to one pointer per program). const ANIM_MAX_CLIPS: int = 32 -var anim_clip_names: pointers = null # clip name per slot -var anim_clip_fps: words = null -var anim_clip_frames: words = null -var anim_clip_mode: words = null -var anim_nclips: int = 0 +export state RtSystemsState { + anim_clip_names: pointers = null # clip name per slot + anim_clip_fps: words = null + anim_clip_frames: words = null + anim_clip_mode: words = null + anim_nclips: int = 0 +} -function anim_clip_init() -> void { - if anim_clip_names == null { - anim_clip_names = pointers(ANIM_MAX_CLIPS) # a pointer (8 bytes) per slot - anim_clip_fps = words(ANIM_MAX_CLIPS) - anim_clip_frames = words(ANIM_MAX_CLIPS) - anim_clip_mode = words(ANIM_MAX_CLIPS) +function anim_clip_init(rt_systems_st: mut RtSystemsState) -> void { + if rt_systems_st.anim_clip_names == null { + rt_systems_st.anim_clip_names = pointers(ANIM_MAX_CLIPS) # a pointer (8 bytes) per slot + rt_systems_st.anim_clip_fps = words(ANIM_MAX_CLIPS) + rt_systems_st.anim_clip_frames = words(ANIM_MAX_CLIPS) + rt_systems_st.anim_clip_mode = words(ANIM_MAX_CLIPS) } } # register (or update) a named clip. mode is the SpriteAnim mode: 0 loop, 1 once, # 2 ping-pong. -function anim_clip(name: pointer, frames: int, fps: int, mode: int) -> void { - anim_clip_init() +function anim_clip(rt_systems_st: mut RtSystemsState, name: pointer, frames: int, fps: int, mode: int) -> void { + anim_clip_init(rt_systems_st) var i = 0 - while i < anim_nclips { - if anim_clip_names[i] == name { - anim_clip_frames[i] = frames; anim_clip_fps[i] = fps; anim_clip_mode[i] = mode + while i < rt_systems_st.anim_nclips { + if rt_systems_st.anim_clip_names[i] == name { + rt_systems_st.anim_clip_frames[i] = frames; rt_systems_st.anim_clip_fps[i] = fps; rt_systems_st.anim_clip_mode[i] = mode return } i += 1 } - if anim_nclips >= ANIM_MAX_CLIPS { return } # silently ignore past capacity - let s = anim_nclips - anim_clip_names[s] = name - anim_clip_frames[s] = frames - anim_clip_fps[s] = fps - anim_clip_mode[s] = mode - anim_nclips += 1 + if rt_systems_st.anim_nclips >= ANIM_MAX_CLIPS { return } # silently ignore past capacity + let s = rt_systems_st.anim_nclips + rt_systems_st.anim_clip_names[s] = name + rt_systems_st.anim_clip_frames[s] = frames + rt_systems_st.anim_clip_fps[s] = fps + rt_systems_st.anim_clip_mode[s] = mode + rt_systems_st.anim_nclips += 1 } -function anim_clip_find(name: pointer) -> int { - anim_clip_init() +function anim_clip_find(rt_systems_st: mut RtSystemsState, name: pointer) -> int { + anim_clip_init(rt_systems_st) var i = 0 - while i < anim_nclips { - if anim_clip_names[i] == name { return i } + while i < rt_systems_st.anim_nclips { + if rt_systems_st.anim_clip_names[i] == name { return i } i += 1 } return -1 @@ -105,10 +107,10 @@ function anim_play(e: int, fps: int, frames: int, mode: int) -> void { } # start a registered clip by name (a no-op if the name is unknown). -function anim_play_named(e: int, name: pointer) -> void { - let c = anim_clip_find(name) +function anim_play_named(rt_systems_st: mut RtSystemsState, e: int, name: pointer) -> void { + let c = anim_clip_find(rt_systems_st, name) if c < 0 { return } - anim_play(e, anim_clip_fps[c], anim_clip_frames[c], anim_clip_mode[c]) + anim_play(e, rt_systems_st.anim_clip_fps[c], rt_systems_st.anim_clip_frames[c], rt_systems_st.anim_clip_mode[c]) } # arm a frame event: the engine flags SpriteAnim.event_fired = 1 on the tick the diff --git a/runtime/native/systems_light.ludic b/runtime/native/systems_light.ludic index 705245b0..14d5b80d 100644 --- a/runtime/native/systems_light.ludic +++ b/runtime/native/systems_light.ludic @@ -51,7 +51,7 @@ function esys_pos_y(e: int, p: int, fy: int) -> int { return 0 } -function esys_light2d() -> void { +function esys_light2d(rt_core_st: mut RtCoreState, rt_light_st: mut RtLightState) -> void { let pl = World.prop_id("Light2D") if pl < 0 { return } @@ -62,12 +62,12 @@ function esys_light2d() -> void { let af = World.field_id(pa, "color") let ae = World.query_next(pa, 0) if ae >= 0 { - if af >= 0 { light_ambient(World.get(ae, pa, af)) } + if af >= 0 { light_ambient(rt_core_st, World.get(ae, pa, af)) } } } # 2. occluders — re-register every `Occluder` as a shadow caster this frame. - light_clear_occluders() + light_clear_occluders(rt_light_st) let po = World.prop_id("Occluder") if po >= 0 { let oxf = World.field_id(po, "x") @@ -82,7 +82,7 @@ function esys_light2d() -> void { var oh = 0 if owf >= 0 { ow = World.get(oe, po, owf) } if ohf >= 0 { oh = World.get(oe, po, ohf) } - light_occlude(ox, oy, ow, oh) + light_occlude(rt_light_st, ox, oy, ow, oh) oe = World.query_next(po, oe + 1) } } @@ -120,18 +120,18 @@ function esys_light2d() -> void { # cone/soft light does not bleed its settings onto the next. var falloff = 1 if lff >= 0 { falloff = World.get(e, pl, lff) } - light_set_falloff(falloff) + light_set_falloff(rt_light_st, falloff) var soft = 0 if lkf >= 0 { soft = World.get(e, pl, lkf) } - light_set_soft(soft) - if lgf >= 0 { light_set_gel(World.get(e, pl, lgf)) } else { light_clear_gel() } + light_set_soft(rt_light_st, soft) + if lgf >= 0 { light_set_gel(rt_light_st, World.get(e, pl, lgf)) } else { light_clear_gel(rt_light_st) } var spread = -1 if lsf >= 0 { spread = World.get(e, pl, lsf) } var direction = 0 if ldf >= 0 { direction = World.get(e, pl, ldf) } - if spread >= 0 { light_spot(x, y, radius, color, energy, direction, spread) } - else { light_point(x, y, radius, color, energy) } + if spread >= 0 { light_spot(rt_core_st, rt_light_st, x, y, radius, color, energy, direction, spread) } + else { light_point(rt_core_st, rt_light_st, x, y, radius, color, energy) } e = World.query_next(pl, e + 1) } diff --git a/runtime/native/systems_move.ludic b/runtime/native/systems_move.ludic index 95a2755b..7ab0801f 100644 --- a/runtime/native/systems_move.ludic +++ b/runtime/native/systems_move.ludic @@ -60,35 +60,37 @@ const PHYS_ONE: int = 65536 # Q16.16 one whole pixel # ---- per-frame config + reflected ids (filled at the top of esys_move) ------- -var pm_pos: int = -1 # Position prop id -var pm_pos_x: int = -1 -var pm_pos_y: int = -1 -var pm_col: int = -1 # Collider prop id -var pm_c_w: int = -1 -var pm_c_h: int = -1 -var pm_c_offx: int = -1 -var pm_c_offy: int = -1 -var pm_c_trig: int = -1 -var pm_c_oneway: int = -1 -var pm_c_layer: int = -1 -var pm_c_mask: int = -1 -var phys_ts: int = 0 # tile size px (0 = grid off) -var phys_wall: int = 0 # solid glyph -var phys_wall2: int = 0 # optional second solid glyph (Solids.solid2), e.g. a closed door -var phys_oneway: int = 0 # one-way platform glyph (0 = none) +export state RtSystemsMoveState { + pm_pos: int = -1 # Position prop id + pm_pos_x: int = -1 + pm_pos_y: int = -1 + pm_col: int = -1 # Collider prop id + pm_c_w: int = -1 + pm_c_h: int = -1 + pm_c_offx: int = -1 + pm_c_offy: int = -1 + pm_c_trig: int = -1 + pm_c_oneway: int = -1 + pm_c_layer: int = -1 + pm_c_mask: int = -1 + phys_ts: int = 0 # tile size px (0 = grid off) + phys_wall: int = 0 # solid glyph + phys_wall2: int = 0 # optional second solid glyph (Solids.solid2), e.g. a closed door + phys_oneway: int = 0 # one-way platform glyph (0 = none) + phys_cur_layer: int = 0 + phys_cur_mask: int = 0 + phys_hit_x: int = 0 # X sweep blocked + phys_hit_up: int = 0 # Y sweep blocked moving up + phys_hit_down: int = 0 # Y sweep blocked moving down + phys_trig_prev: words = null +} # the moving body's own layer/mask, so the solid scan can filter by them -var phys_cur_layer: int = 0 -var phys_cur_mask: int = 0 # sweep outputs (Ludic returns one value, so axis clampers report contact here) -var phys_hit_x: int = 0 # X sweep blocked -var phys_hit_up: int = 0 # Y sweep blocked moving up -var phys_hit_down: int = 0 # Y sweep blocked moving down # trigger edge tracking: the body a trigger overlapped last frame, stored as # (entity id + 1) so a zero-filled slot reads as "none" (entity 0 is valid). -var phys_trig_prev: words = null # ---- small integer helpers -------------------------------------------------- # floor division toward negative infinity (positions can go slightly negative); @@ -102,42 +104,42 @@ function phys_floordiv(a: int, b: int) -> int { # a solid (fully blocking) tile: out of bounds counts as a wall, so the map edge # is a free arena boundary; otherwise the glyph equals the configured wall. -function phys_tile_solid(c: int, r: int) -> bool { +function phys_tile_solid(rt_core_st: RtCoreState, rt_systems_move_st: RtSystemsMoveState, c: int, r: int) -> bool { if c < 0 { return true } if r < 0 { return true } - if c >= rt_mapw { return true } - if r >= rt_maph { return true } - let g = rt_tile(c, r) - if g == phys_wall { return true } - return (phys_wall2 != 0) and (g == phys_wall2) + if c >= rt_core_st.rt_mapw { return true } + if r >= rt_core_st.rt_maph { return true } + let g = rt_tile(rt_core_st, c, r) + if g == rt_systems_move_st.phys_wall { return true } + return (rt_systems_move_st.phys_wall2 != 0) and (g == rt_systems_move_st.phys_wall2) } # a one-way platform tile (in bounds only — the arena edge is a full wall above). -function phys_tile_oneway(c: int, r: int) -> bool { - if phys_oneway == 0 { return false } +function phys_tile_oneway(rt_core_st: RtCoreState, rt_systems_move_st: RtSystemsMoveState, c: int, r: int) -> bool { + if rt_systems_move_st.phys_oneway == 0 { return false } if c < 0 { return false } if r < 0 { return false } - if c >= rt_mapw { return false } - if r >= rt_maph { return false } - return rt_tile(c, r) == phys_oneway + if c >= rt_core_st.rt_mapw { return false } + if r >= rt_core_st.rt_maph { return false } + return rt_tile(rt_core_st, c, r) == rt_systems_move_st.phys_oneway } # is any full-solid tile present in column c over the row span [r0, r1]? -function phys_col_solid(c: int, r0: int, r1: int) -> bool { +function phys_col_solid(rt_core_st: RtCoreState, rt_systems_move_st: RtSystemsMoveState, c: int, r0: int, r1: int) -> bool { var r = r0 - while r <= r1 { if phys_tile_solid(c, r) { return true }; r += 1 } + while r <= r1 { if phys_tile_solid(rt_core_st, rt_systems_move_st, c, r) { return true }; r += 1 } return false } # is any full-solid tile present in row r over the column span [c0, c1]? -function phys_row_solid(r: int, c0: int, c1: int) -> bool { +function phys_row_solid(rt_core_st: RtCoreState, rt_systems_move_st: RtSystemsMoveState, r: int, c0: int, c1: int) -> bool { var c = c0 - while c <= c1 { if phys_tile_solid(c, r) { return true }; c += 1 } + while c <= c1 { if phys_tile_solid(rt_core_st, rt_systems_move_st, c, r) { return true }; c += 1 } return false } # is any full-OR-one-way tile present in row r over [c0, c1]? (downward landings) -function phys_row_land(r: int, c0: int, c1: int) -> bool { +function phys_row_land(rt_core_st: RtCoreState, rt_systems_move_st: RtSystemsMoveState, r: int, c0: int, c1: int) -> bool { var c = c0 while c <= c1 { - if phys_tile_solid(c, r) { return true } - if phys_tile_oneway(c, r) { return true } + if phys_tile_solid(rt_core_st, rt_systems_move_st, c, r) { return true } + if phys_tile_oneway(rt_core_st, rt_systems_move_st, c, r) { return true } c += 1 } return false @@ -157,37 +159,37 @@ function phys_match(la: int, ma: int, lb: int, mb: int) -> bool { } # ---- reflected accessors ---------------------------------------------------- -function phys_pos_x(e: int) -> int { return World.get(e, pm_pos, pm_pos_x) } -function phys_pos_y(e: int) -> int { return World.get(e, pm_pos, pm_pos_y) } -function phys_set_x(e: int, v: int) -> void { World.set(e, pm_pos, pm_pos_x, v) } -function phys_set_y(e: int, v: int) -> void { World.set(e, pm_pos, pm_pos_y, v) } +function phys_pos_x(rt_systems_move_st: RtSystemsMoveState, e: int) -> int { return World.get(e, rt_systems_move_st.pm_pos, rt_systems_move_st.pm_pos_x) } +function phys_pos_y(rt_systems_move_st: RtSystemsMoveState, e: int) -> int { return World.get(e, rt_systems_move_st.pm_pos, rt_systems_move_st.pm_pos_y) } +function phys_set_x(rt_systems_move_st: RtSystemsMoveState, e: int, v: int) -> void { World.set(e, rt_systems_move_st.pm_pos, rt_systems_move_st.pm_pos_x, v) } +function phys_set_y(rt_systems_move_st: RtSystemsMoveState, e: int, v: int) -> void { World.set(e, rt_systems_move_st.pm_pos, rt_systems_move_st.pm_pos_y, v) } -function phys_c_int(e: int, f: int) -> int { +function phys_c_int(rt_systems_move_st: RtSystemsMoveState, e: int, f: int) -> int { if f < 0 { return 0 } - return World.get(e, pm_col, f) + return World.get(e, rt_systems_move_st.pm_col, f) } # a collider's AABB left / top edge (Position anchor + offset). -function phys_aabb_x(e: int) -> int { return phys_pos_x(e) + phys_c_int(e, pm_c_offx) } -function phys_aabb_y(e: int) -> int { return phys_pos_y(e) + phys_c_int(e, pm_c_offy) } +function phys_aabb_x(rt_systems_move_st: RtSystemsMoveState, e: int) -> int { return phys_pos_x(rt_systems_move_st, e) + phys_c_int(rt_systems_move_st, e, rt_systems_move_st.pm_c_offx) } +function phys_aabb_y(rt_systems_move_st: RtSystemsMoveState, e: int) -> int { return phys_pos_y(rt_systems_move_st, e) + phys_c_int(rt_systems_move_st, e, rt_systems_move_st.pm_c_offy) } # ---- X sweep ---------------------------------------------------------------- # Clamp a body's tentative dx so its AABB (px,py,w,h) stops at the first solid it # would cross this frame — entity colliders and (optionally) the tile grid — and # flag phys_hit_x on contact. One-way platforms never block horizontal motion. -function phys_clamp_x(self_e: int, px: int, py: int, w: int, h: int, dx: int) -> int { +function phys_clamp_x(rt_core_st: RtCoreState, rt_systems_move_st: mut RtSystemsMoveState, self_e: int, px: int, py: int, w: int, h: int, dx: int) -> int { if dx == 0 { return 0 } var best = dx # --- entity solids: every other non-trigger collider we match against - if pm_col >= 0 { - var s = World.query_next(pm_col, 0) + if rt_systems_move_st.pm_col >= 0 { + var s = World.query_next(rt_systems_move_st.pm_col, 0) while s >= 0 { if s != self_e { - if phys_c_int(s, pm_c_trig) == 0 { - if phys_match(phys_cur_layer, phys_cur_mask, phys_c_int(s, pm_c_layer), phys_c_int(s, pm_c_mask)) { - if phys_c_int(s, pm_c_oneway) == 0 { # one-way: pass horizontally - let sx = phys_aabb_x(s); let sy = phys_aabb_y(s) - let sw = phys_c_int(s, pm_c_w); let sh = phys_c_int(s, pm_c_h) + if phys_c_int(rt_systems_move_st, s, rt_systems_move_st.pm_c_trig) == 0 { + if phys_match(rt_systems_move_st.phys_cur_layer, rt_systems_move_st.phys_cur_mask, phys_c_int(rt_systems_move_st, s, rt_systems_move_st.pm_c_layer), phys_c_int(rt_systems_move_st, s, rt_systems_move_st.pm_c_mask)) { + if phys_c_int(rt_systems_move_st, s, rt_systems_move_st.pm_c_oneway) == 0 { # one-way: pass horizontally + let sx = phys_aabb_x(rt_systems_move_st, s); let sy = phys_aabb_y(rt_systems_move_st, s) + let sw = phys_c_int(rt_systems_move_st, s, rt_systems_move_st.pm_c_w); let sh = phys_c_int(rt_systems_move_st, s, rt_systems_move_st.pm_c_h) if (py < sy + sh) and (sy < py + h) { # vertical spans overlap if dx > 0 { if px + w <= sx { # currently left of s @@ -205,13 +207,13 @@ function phys_clamp_x(self_e: int, px: int, py: int, w: int, h: int, dx: int) -> } } } - s = World.query_next(pm_col, s + 1) + s = World.query_next(rt_systems_move_st.pm_col, s + 1) } } # --- tile grid: nearest solid column in the swept path over the row span - if phys_ts > 0 { - let ts = phys_ts + if rt_systems_move_st.phys_ts > 0 { + let ts = rt_systems_move_st.phys_ts let r0 = phys_floordiv(py, ts) let r1 = phys_floordiv(py + h - 1, ts) if dx > 0 { @@ -219,7 +221,7 @@ function phys_clamp_x(self_e: int, px: int, py: int, w: int, h: int, dx: int) -> let endc = phys_floordiv(px + w - 1 + best, ts) var c = startc + 1 while c <= endc { - if phys_col_solid(c, r0, r1) { + if phys_col_solid(rt_core_st, rt_systems_move_st, c, r0, r1) { let allow = c * ts - (px + w) if allow < best { best = allow } c = endc + 1 # nearest wins; stop @@ -230,7 +232,7 @@ function phys_clamp_x(self_e: int, px: int, py: int, w: int, h: int, dx: int) -> let endc = phys_floordiv(px + best, ts) var c = startc - 1 while c >= endc { - if phys_col_solid(c, r0, r1) { + if phys_col_solid(rt_core_st, rt_systems_move_st, c, r0, r1) { let allow = (c + 1) * ts - px if allow > best { best = allow } c = endc - 1 # nearest wins; stop @@ -239,7 +241,7 @@ function phys_clamp_x(self_e: int, px: int, py: int, w: int, h: int, dx: int) -> } } - if best != dx { phys_hit_x = 1 } + if best != dx { rt_systems_move_st.phys_hit_x = 1 } return best } @@ -247,19 +249,19 @@ function phys_clamp_x(self_e: int, px: int, py: int, w: int, h: int, dx: int) -> # Clamp a body's tentative dy against solids (and one-way platforms on a downward # landing), flagging phys_hit_down / phys_hit_up. Must run after the X sweep so # px is already resolved for this frame. -function phys_clamp_y(self_e: int, px: int, py: int, w: int, h: int, dy: int) -> int { +function phys_clamp_y(rt_core_st: RtCoreState, rt_systems_move_st: mut RtSystemsMoveState, self_e: int, px: int, py: int, w: int, h: int, dy: int) -> int { if dy == 0 { return 0 } var best = dy - if pm_col >= 0 { - var s = World.query_next(pm_col, 0) + if rt_systems_move_st.pm_col >= 0 { + var s = World.query_next(rt_systems_move_st.pm_col, 0) while s >= 0 { if s != self_e { - if phys_c_int(s, pm_c_trig) == 0 { - if phys_match(phys_cur_layer, phys_cur_mask, phys_c_int(s, pm_c_layer), phys_c_int(s, pm_c_mask)) { - let sx = phys_aabb_x(s); let sy = phys_aabb_y(s) - let sw = phys_c_int(s, pm_c_w); let sh = phys_c_int(s, pm_c_h) - let oneway = phys_c_int(s, pm_c_oneway) + if phys_c_int(rt_systems_move_st, s, rt_systems_move_st.pm_c_trig) == 0 { + if phys_match(rt_systems_move_st.phys_cur_layer, rt_systems_move_st.phys_cur_mask, phys_c_int(rt_systems_move_st, s, rt_systems_move_st.pm_c_layer), phys_c_int(rt_systems_move_st, s, rt_systems_move_st.pm_c_mask)) { + let sx = phys_aabb_x(rt_systems_move_st, s); let sy = phys_aabb_y(rt_systems_move_st, s) + let sw = phys_c_int(rt_systems_move_st, s, rt_systems_move_st.pm_c_w); let sh = phys_c_int(rt_systems_move_st, s, rt_systems_move_st.pm_c_h) + let oneway = phys_c_int(rt_systems_move_st, s, rt_systems_move_st.pm_c_oneway) if (px < sx + sw) and (sx < px + w) { # horizontal spans overlap if dy > 0 { # a one-way platform blocks only when the body starts on/above it @@ -283,12 +285,12 @@ function phys_clamp_y(self_e: int, px: int, py: int, w: int, h: int, dy: int) -> } } } - s = World.query_next(pm_col, s + 1) + s = World.query_next(rt_systems_move_st.pm_col, s + 1) } } - if phys_ts > 0 { - let ts = phys_ts + if rt_systems_move_st.phys_ts > 0 { + let ts = rt_systems_move_st.phys_ts let c0 = phys_floordiv(px, ts) let c1 = phys_floordiv(px + w - 1, ts) if dy > 0 { @@ -296,10 +298,10 @@ function phys_clamp_y(self_e: int, px: int, py: int, w: int, h: int, dy: int) -> let endr = phys_floordiv(py + h - 1 + best, ts) var r = startr + 1 while r <= endr { - var block = phys_row_solid(r, c0, c1) + var block = phys_row_solid(rt_core_st, rt_systems_move_st, r, c0, c1) # one-way tiles only catch a body whose feet start at/above the tile top if not block { - if phys_row_land(r, c0, c1) { if py + h <= r * ts { block = true } } + if phys_row_land(rt_core_st, rt_systems_move_st, r, c0, c1) { if py + h <= r * ts { block = true } } } if block { let allow = r * ts - (py + h) @@ -312,7 +314,7 @@ function phys_clamp_y(self_e: int, px: int, py: int, w: int, h: int, dy: int) -> let endr = phys_floordiv(py + best, ts) var r = startr - 1 while r >= endr { - if phys_row_solid(r, c0, c1) { # one-way never blocks upward + if phys_row_solid(rt_core_st, rt_systems_move_st, r, c0, c1) { # one-way never blocks upward let allow = (r + 1) * ts - py if allow > best { best = allow } r = endr - 1 @@ -322,7 +324,7 @@ function phys_clamp_y(self_e: int, px: int, py: int, w: int, h: int, dy: int) -> } if best != dy { - if dy > 0 { phys_hit_down = 1 } else { phys_hit_up = 1 } + if dy > 0 { rt_systems_move_st.phys_hit_down = 1 } else { rt_systems_move_st.phys_hit_up = 1 } } return best } @@ -330,40 +332,40 @@ function phys_clamp_y(self_e: int, px: int, py: int, w: int, h: int, dy: int) -> # ---- ground probe ----------------------------------------------------------- # on_ground is a 1px downward overlap test, independent of this frame's velocity, # so a body resting under sub-pixel gravity still reads grounded. -function phys_grounded(self_e: int, px: int, py: int, w: int, h: int) -> bool { +function phys_grounded(rt_core_st: RtCoreState, rt_systems_move_st: RtSystemsMoveState, self_e: int, px: int, py: int, w: int, h: int) -> bool { # entity solids one pixel below - if pm_col >= 0 { - var s = World.query_next(pm_col, 0) + if rt_systems_move_st.pm_col >= 0 { + var s = World.query_next(rt_systems_move_st.pm_col, 0) while s >= 0 { if s != self_e { - if phys_c_int(s, pm_c_trig) == 0 { - if phys_match(phys_cur_layer, phys_cur_mask, phys_c_int(s, pm_c_layer), phys_c_int(s, pm_c_mask)) { - let sx = phys_aabb_x(s); let sy = phys_aabb_y(s) - let sw = phys_c_int(s, pm_c_w); let sh = phys_c_int(s, pm_c_h) + if phys_c_int(rt_systems_move_st, s, rt_systems_move_st.pm_c_trig) == 0 { + if phys_match(rt_systems_move_st.phys_cur_layer, rt_systems_move_st.phys_cur_mask, phys_c_int(rt_systems_move_st, s, rt_systems_move_st.pm_c_layer), phys_c_int(rt_systems_move_st, s, rt_systems_move_st.pm_c_mask)) { + let sx = phys_aabb_x(rt_systems_move_st, s); let sy = phys_aabb_y(rt_systems_move_st, s) + let sw = phys_c_int(rt_systems_move_st, s, rt_systems_move_st.pm_c_w); let sh = phys_c_int(rt_systems_move_st, s, rt_systems_move_st.pm_c_h) if (px < sx + sw) and (sx < px + w) { if (py + h <= sy) and (py + h + 1 > sy) { return true } } } } } - s = World.query_next(pm_col, s + 1) + s = World.query_next(rt_systems_move_st.pm_col, s + 1) } } - if phys_ts > 0 { - let ts = phys_ts + if rt_systems_move_st.phys_ts > 0 { + let ts = rt_systems_move_st.phys_ts let c0 = phys_floordiv(px, ts) let c1 = phys_floordiv(px + w - 1, ts) let footr = phys_floordiv(py + h, ts) # tile row just below the feet if (py + h) == footr * ts { # feet flush on a tile boundary - if phys_row_solid(footr, c0, c1) { return true } - if phys_row_land(footr, c0, c1) { return true } + if phys_row_solid(rt_core_st, rt_systems_move_st, footr, c0, c1) { return true } + if phys_row_land(rt_core_st, rt_systems_move_st, footr, c0, c1) { return true } } } return false } # ---- the system ------------------------------------------------------------ -function esys_move() -> void { +function esys_move(rt_core_st: mut RtCoreState, rt_systems_move_st: mut RtSystemsMoveState) -> void { let pb = World.prop_id("Body") if pb < 0 { return } @@ -382,23 +384,23 @@ function esys_move() -> void { let f_ny = World.field_id(pb, "hit_ny") # shared / config props (into module globals the helpers read) - pm_pos = World.prop_id("Position") - if pm_pos >= 0 { - pm_pos_x = World.field_id(pm_pos, "x") - pm_pos_y = World.field_id(pm_pos, "y") + rt_systems_move_st.pm_pos = World.prop_id("Position") + if rt_systems_move_st.pm_pos >= 0 { + rt_systems_move_st.pm_pos_x = World.field_id(rt_systems_move_st.pm_pos, "x") + rt_systems_move_st.pm_pos_y = World.field_id(rt_systems_move_st.pm_pos, "y") } - pm_col = World.prop_id("Collider") - if pm_col >= 0 { - pm_c_w = World.field_id(pm_col, "w") - pm_c_h = World.field_id(pm_col, "h") - pm_c_offx = World.field_id(pm_col, "offx") - pm_c_offy = World.field_id(pm_col, "offy") - pm_c_trig = World.field_id(pm_col, "is_trigger") - pm_c_oneway = World.field_id(pm_col, "one_way") - pm_c_layer = World.field_id(pm_col, "layer") - pm_c_mask = World.field_id(pm_col, "mask") + rt_systems_move_st.pm_col = World.prop_id("Collider") + if rt_systems_move_st.pm_col >= 0 { + rt_systems_move_st.pm_c_w = World.field_id(rt_systems_move_st.pm_col, "w") + rt_systems_move_st.pm_c_h = World.field_id(rt_systems_move_st.pm_col, "h") + rt_systems_move_st.pm_c_offx = World.field_id(rt_systems_move_st.pm_col, "offx") + rt_systems_move_st.pm_c_offy = World.field_id(rt_systems_move_st.pm_col, "offy") + rt_systems_move_st.pm_c_trig = World.field_id(rt_systems_move_st.pm_col, "is_trigger") + rt_systems_move_st.pm_c_oneway = World.field_id(rt_systems_move_st.pm_col, "one_way") + rt_systems_move_st.pm_c_layer = World.field_id(rt_systems_move_st.pm_col, "layer") + rt_systems_move_st.pm_c_mask = World.field_id(rt_systems_move_st.pm_col, "mask") } - phys_ts = 0; phys_wall = 0; phys_wall2 = 0; phys_oneway = 0 + rt_systems_move_st.phys_ts = 0; rt_systems_move_st.phys_wall = 0; rt_systems_move_st.phys_wall2 = 0; rt_systems_move_st.phys_oneway = 0 let ps = World.prop_id("Solids") if ps >= 0 { let se = World.query_next(ps, 0) @@ -406,24 +408,24 @@ function esys_move() -> void { let f_tile = World.field_id(ps, "tile") let f_wall = World.field_id(ps, "wall") let f_ow = World.field_id(ps, "oneway") - if f_tile >= 0 { phys_ts = World.get(se, ps, f_tile) } - if f_wall >= 0 { phys_wall = World.get(se, ps, f_wall) } - if f_ow >= 0 { phys_oneway = World.get(se, ps, f_ow) } + if f_tile >= 0 { rt_systems_move_st.phys_ts = World.get(se, ps, f_tile) } + if f_wall >= 0 { rt_systems_move_st.phys_wall = World.get(se, ps, f_wall) } + if f_ow >= 0 { rt_systems_move_st.phys_oneway = World.get(se, ps, f_ow) } let f_w2 = World.field_id(ps, "solid2") - if f_w2 >= 0 { phys_wall2 = World.get(se, ps, f_w2) } - rt_map_solid1 = phys_wall; rt_map_solid2 = phys_wall2 # Map.is_solid reads the same config - if phys_ts > 0 { rt_map_tile_px = phys_ts } + if f_w2 >= 0 { rt_systems_move_st.phys_wall2 = World.get(se, ps, f_w2) } + rt_core_st.rt_map_solid1 = rt_systems_move_st.phys_wall; rt_core_st.rt_map_solid2 = rt_systems_move_st.phys_wall2 # Map.is_solid reads the same config + if rt_systems_move_st.phys_ts > 0 { rt_core_st.rt_map_tile_px = rt_systems_move_st.phys_ts } } } # a Position is required to move a body; a Body with none is inert. - if pm_pos < 0 { return } - if pm_pos_x < 0 { return } - if pm_pos_y < 0 { return } + if rt_systems_move_st.pm_pos < 0 { return } + if rt_systems_move_st.pm_pos_x < 0 { return } + if rt_systems_move_st.pm_pos_y < 0 { return } var e = World.query_next(pb, 0) while e >= 0 { - if World.has(e, pm_pos) != 0 { + if World.has(e, rt_systems_move_st.pm_pos) != 0 { # 1. integrate velocity (+ gravity for a platformer policy) var vx = 0; if f_vx >= 0 { vx = World.get(e, pb, f_vx) } var vy = 0; if f_vy >= 0 { vy = World.get(e, pb, f_vy) } @@ -452,42 +454,42 @@ function esys_move() -> void { # 3. does this body have a solid shape? (a trigger-only body moves freely) var has_shape = false - if pm_col >= 0 { - if World.has(e, pm_col) != 0 { if phys_c_int(e, pm_c_trig) == 0 { has_shape = true } } + if rt_systems_move_st.pm_col >= 0 { + if World.has(e, rt_systems_move_st.pm_col) != 0 { if phys_c_int(rt_systems_move_st, e, rt_systems_move_st.pm_c_trig) == 0 { has_shape = true } } } if has_shape { - phys_cur_layer = phys_c_int(e, pm_c_layer) - phys_cur_mask = phys_c_int(e, pm_c_mask) - let w = phys_c_int(e, pm_c_w) - let h = phys_c_int(e, pm_c_h) - var px = phys_aabb_x(e) - let py0 = phys_aabb_y(e) + rt_systems_move_st.phys_cur_layer = phys_c_int(rt_systems_move_st, e, rt_systems_move_st.pm_c_layer) + rt_systems_move_st.phys_cur_mask = phys_c_int(rt_systems_move_st, e, rt_systems_move_st.pm_c_mask) + let w = phys_c_int(rt_systems_move_st, e, rt_systems_move_st.pm_c_w) + let h = phys_c_int(rt_systems_move_st, e, rt_systems_move_st.pm_c_h) + var px = phys_aabb_x(rt_systems_move_st, e) + let py0 = phys_aabb_y(rt_systems_move_st, e) # X sweep, commit, then Y sweep from the resolved x - phys_hit_x = 0 - let cdx = phys_clamp_x(e, px, py0, w, h, dx) - phys_set_x(e, phys_pos_x(e) + cdx) - px = phys_aabb_x(e) + rt_systems_move_st.phys_hit_x = 0 + let cdx = phys_clamp_x(rt_core_st, rt_systems_move_st, e, px, py0, w, h, dx) + phys_set_x(rt_systems_move_st, e, phys_pos_x(rt_systems_move_st, e) + cdx) + px = phys_aabb_x(rt_systems_move_st, e) - phys_hit_up = 0; phys_hit_down = 0 - let py = phys_aabb_y(e) - let cdy = phys_clamp_y(e, px, py, w, h, dy) - phys_set_y(e, phys_pos_y(e) + cdy) + rt_systems_move_st.phys_hit_up = 0; rt_systems_move_st.phys_hit_down = 0 + let py = phys_aabb_y(rt_systems_move_st, e) + let cdy = phys_clamp_y(rt_core_st, rt_systems_move_st, e, px, py, w, h, dy) + phys_set_y(rt_systems_move_st, e, phys_pos_y(rt_systems_move_st, e) + cdy) # derived flags + contact normal - if phys_hit_x == 1 { hit_wall = 1; vx = 0; if dx > 0 { nx = -1 } else { nx = 1 } } - if phys_hit_up == 1 { hit_ceiling = 1; vy = 0; ny = 1 } - if phys_hit_down == 1 { vy = 0; ny = -1 } - let gy = phys_aabb_y(e) - if phys_grounded(e, px, gy, w, h) { on_ground = 1 } + if rt_systems_move_st.phys_hit_x == 1 { hit_wall = 1; vx = 0; if dx > 0 { nx = -1 } else { nx = 1 } } + if rt_systems_move_st.phys_hit_up == 1 { hit_ceiling = 1; vy = 0; ny = 1 } + if rt_systems_move_st.phys_hit_down == 1 { vy = 0; ny = -1 } + let gy = phys_aabb_y(rt_systems_move_st, e) + if phys_grounded(rt_core_st, rt_systems_move_st, e, px, gy, w, h) { on_ground = 1 } # a body pressed onto the ground carries no downward velocity, whatever # the gravity step was this frame (so on_ground implies a settled vy). if (on_ground == 1) and (vy > 0) { vy = 0 } } else { # free integration: apply the whole-pixel step straight to Position - phys_set_x(e, phys_pos_x(e) + dx) - phys_set_y(e, phys_pos_y(e) + dy) + phys_set_x(rt_systems_move_st, e, phys_pos_x(rt_systems_move_st, e) + dx) + phys_set_y(rt_systems_move_st, e, phys_pos_y(rt_systems_move_st, e) + dy) } # 4. write velocity + subpixel remainder + derived outputs back @@ -511,35 +513,35 @@ function esys_move() -> void { # 5. triggers/sensors — overlap that reports but never resolves. For each # trigger collider, find the lowest-id body-shaped collider it overlaps and # edge-detect enter/exit against last frame (SpriteAnim.event_fired shape). - esys_triggers() + esys_triggers(rt_systems_move_st) } -function esys_triggers() -> void { - if pm_col < 0 { return } - if pm_c_trig < 0 { return } - let f_hit = World.field_id(pm_col, "hit") - let f_ent = World.field_id(pm_col, "entered") - let f_ext = World.field_id(pm_col, "exited") +function esys_triggers(rt_systems_move_st: mut RtSystemsMoveState) -> void { + if rt_systems_move_st.pm_col < 0 { return } + if rt_systems_move_st.pm_c_trig < 0 { return } + let f_hit = World.field_id(rt_systems_move_st.pm_col, "hit") + let f_ent = World.field_id(rt_systems_move_st.pm_col, "entered") + let f_ext = World.field_id(rt_systems_move_st.pm_col, "exited") if (f_hit < 0) and (f_ent < 0) and (f_ext < 0) { return } # no trigger outputs declared - if phys_trig_prev == null { phys_trig_prev = words(1024) } + if rt_systems_move_st.phys_trig_prev == null { rt_systems_move_st.phys_trig_prev = words(1024) } - var t = World.query_next(pm_col, 0) + var t = World.query_next(rt_systems_move_st.pm_col, 0) while t >= 0 { - if phys_c_int(t, pm_c_trig) != 0 { - if World.has(t, pm_pos) != 0 { - let tx = phys_aabb_x(t); let ty = phys_aabb_y(t) - let tw = phys_c_int(t, pm_c_w); let th = phys_c_int(t, pm_c_h) - let tl = phys_c_int(t, pm_c_layer); let tm = phys_c_int(t, pm_c_mask) + if phys_c_int(rt_systems_move_st, t, rt_systems_move_st.pm_c_trig) != 0 { + if World.has(t, rt_systems_move_st.pm_pos) != 0 { + let tx = phys_aabb_x(rt_systems_move_st, t); let ty = phys_aabb_y(rt_systems_move_st, t) + let tw = phys_c_int(rt_systems_move_st, t, rt_systems_move_st.pm_c_w); let th = phys_c_int(rt_systems_move_st, t, rt_systems_move_st.pm_c_h) + let tl = phys_c_int(rt_systems_move_st, t, rt_systems_move_st.pm_c_layer); let tm = phys_c_int(rt_systems_move_st, t, rt_systems_move_st.pm_c_mask) var cur = -1 - var b = World.query_next(pm_col, 0) + var b = World.query_next(rt_systems_move_st.pm_col, 0) while b >= 0 { if b != t { - if phys_c_int(b, pm_c_trig) == 0 { - if World.has(b, pm_pos) != 0 { - if phys_match(tl, tm, phys_c_int(b, pm_c_layer), phys_c_int(b, pm_c_mask)) { - let bx = phys_aabb_x(b); let by = phys_aabb_y(b) - let bw = phys_c_int(b, pm_c_w); let bh = phys_c_int(b, pm_c_h) + if phys_c_int(rt_systems_move_st, b, rt_systems_move_st.pm_c_trig) == 0 { + if World.has(b, rt_systems_move_st.pm_pos) != 0 { + if phys_match(tl, tm, phys_c_int(rt_systems_move_st, b, rt_systems_move_st.pm_c_layer), phys_c_int(rt_systems_move_st, b, rt_systems_move_st.pm_c_mask)) { + let bx = phys_aabb_x(rt_systems_move_st, b); let by = phys_aabb_y(rt_systems_move_st, b) + let bw = phys_c_int(rt_systems_move_st, b, rt_systems_move_st.pm_c_w); let bh = phys_c_int(rt_systems_move_st, b, rt_systems_move_st.pm_c_h) if (tx < bx + bw) and (bx < tx + tw) and (ty < by + bh) and (by < ty + th) { if cur < 0 { cur = b } # lowest id wins (deterministic) } @@ -547,19 +549,19 @@ function esys_triggers() -> void { } } } - b = World.query_next(pm_col, b + 1) + b = World.query_next(rt_systems_move_st.pm_col, b + 1) } - let prev = phys_trig_prev[t] - 1 # stored as id+1, 0 = none + let prev = rt_systems_move_st.phys_trig_prev[t] - 1 # stored as id+1, 0 = none var entered = 0 var exited = 0 if (prev < 0) and (cur >= 0) { entered = 1 } if (prev >= 0) and (cur < 0) { exited = 1 } - phys_trig_prev[t] = cur + 1 - if f_hit >= 0 { World.set(t, pm_col, f_hit, cur) } - if f_ent >= 0 { World.set(t, pm_col, f_ent, entered) } - if f_ext >= 0 { World.set(t, pm_col, f_ext, exited) } + rt_systems_move_st.phys_trig_prev[t] = cur + 1 + if f_hit >= 0 { World.set(t, rt_systems_move_st.pm_col, f_hit, cur) } + if f_ent >= 0 { World.set(t, rt_systems_move_st.pm_col, f_ent, entered) } + if f_ext >= 0 { World.set(t, rt_systems_move_st.pm_col, f_ext, exited) } } } - t = World.query_next(pm_col, t + 1) + t = World.query_next(rt_systems_move_st.pm_col, t + 1) } } diff --git a/runtime/native/systems_sprite.ludic b/runtime/native/systems_sprite.ludic index d8aa7ea5..73b925df 100644 --- a/runtime/native/systems_sprite.ludic +++ b/runtime/native/systems_sprite.ludic @@ -43,7 +43,7 @@ function spr_pos(e: int, axis: int) -> int { return World.get(e, pp, f) } -function esys_sprite() -> void { +function esys_sprite(rt_atlas_st: mut RtAtlasState, rt_core_st: mut RtCoreState, rt_image_st: RtImageState) -> void { let P = World.prop_id("Sprite") if P < 0 { return } let f_id = World.field_id(P, "id") @@ -109,8 +109,8 @@ function esys_sprite() -> void { if (f_flash >= 0) and (World.get(e, P, f_flash) > 0) { tint = 0xffffff } var is_atlas = 0 if f_atlas >= 0 { is_atlas = World.get(e, P, f_atlas) } - if is_atlas != 0 { atlas_draw_ex(id, x, y, sc, flip, tint) } # #90 atlas cells / spans - else { rt_draw_sprite_ex(id, x, y, sc, flip, tint) } + if is_atlas != 0 { atlas_draw_ex(rt_atlas_st, rt_core_st, rt_image_st, id, x, y, sc, flip, tint) } # #90 atlas cells / spans + else { rt_draw_sprite_ex(rt_core_st, rt_image_st, id, x, y, sc, flip, tint) } } e = World.query_next(P, e + 1) } diff --git a/runtime/native/systems_tileskin.ludic b/runtime/native/systems_tileskin.ludic index f36c4d56..043ce797 100644 --- a/runtime/native/systems_tileskin.ludic +++ b/runtime/native/systems_tileskin.ludic @@ -16,7 +16,7 @@ # ============================================================================ import "atlas.ludic" -function esys_tileskin() -> void { +function esys_tileskin(rt_atlas_st: mut RtAtlasState, rt_core_st: mut RtCoreState, rt_image_st: RtImageState) -> void { let P = World.prop_id("TileSkin") if P < 0 { return } let f_glyph = World.field_id(P, "glyph") @@ -36,12 +36,12 @@ function esys_tileskin() -> void { var sc = 1 if f_scale >= 0 { let cv = World.get(e, P, f_scale); if cv > 1 { sc = cv } } var ty = 0 - while ty < rt_maph { + while ty < rt_core_st.rt_maph { var tx = 0 - while tx < rt_mapw { - if rt_tile(tx, ty) == glyph { - if is_atlas != 0 { atlas_draw_ex(sprite, tx * size, ty * size, sc, 0, 0) } - else { rt_draw_sprite_ex(sprite, tx * size, ty * size, sc, 0, 0) } + while tx < rt_core_st.rt_mapw { + if rt_tile(rt_core_st, tx, ty) == glyph { + if is_atlas != 0 { atlas_draw_ex(rt_atlas_st, rt_core_st, rt_image_st, sprite, tx * size, ty * size, sc, 0, 0) } + else { rt_draw_sprite_ex(rt_core_st, rt_image_st, sprite, tx * size, ty * size, sc, 0, 0) } } tx += 1 } diff --git a/runtime/native/tiled.ludic b/runtime/native/tiled.ludic index 6dbf248a..808ac422 100644 --- a/runtime/native/tiled.ludic +++ b/runtime/native/tiled.ludic @@ -41,18 +41,18 @@ function tiled_csv_list(text: pointer) -> Val { # a base64 (optionally zlib/gzip-compressed) blob of `count` little-endian u32 # GIDs -> a Value list of int nodes. -function tiled_b64_list(text: pointer, compression: pointer, count: int) -> Val { +function tiled_b64_list(rt_inflate_st: mut RtInflateState, rt_zstd_st: mut RtZstdState, text: pointer, compression: pointer, count: int) -> Val { let comp = bytes(len(text) + 4) let clen = b64_decode(text, comp) let outcap = count * 4 + 16 var raw = comp var rawlen = clen if compression == "zlib" { - let d = bytes(outcap); let dn = z_uncompress(comp, clen, d, outcap); raw = d; rawlen = dn + let d = bytes(outcap); let dn = z_uncompress(rt_inflate_st, comp, clen, d, outcap); raw = d; rawlen = dn } else { if compression == "gzip" { - let d = bytes(outcap); let dn = z_gunzip(comp, clen, d, outcap); raw = d; rawlen = dn + let d = bytes(outcap); let dn = z_gunzip(rt_inflate_st, comp, clen, d, outcap); raw = d; rawlen = dn } else { if compression == "zstd" { - let d = bytes(outcap); let dn = z_zstd(comp, clen, d, outcap); raw = d; rawlen = dn + let d = bytes(outcap); let dn = z_zstd(rt_zstd_st, comp, clen, d, outcap); raw = d; rawlen = dn } } } let out = value_list() var i = 0 @@ -65,19 +65,19 @@ function tiled_b64_list(text: pointer, compression: pointer, count: int) -> Val } # decode the text of `node` under encoding `enc` / compression `comp` -> GID list. -function tiled_decode_enc(node: Xml, enc: pointer, comp: pointer, count: int) -> Val { - if enc == "base64" { return tiled_b64_list(xml_text(node), comp, count) } +function tiled_decode_enc(rt_inflate_st: mut RtInflateState, rt_zstd_st: mut RtZstdState, node: Xml, enc: pointer, comp: pointer, count: int) -> Val { + if enc == "base64" { return tiled_b64_list(rt_inflate_st, rt_zstd_st, xml_text(node), comp, count) } return tiled_csv_list(xml_text(node)) # csv (or the tag-per-tile form) } # decode a `` element (child of a ``) into a Value list of GIDs. -function tiled_data_list(data: Xml, count: int) -> Val { - return tiled_decode_enc(data, xml_attr(data, "encoding"), xml_attr(data, "compression"), count) +function tiled_data_list(rt_inflate_st: mut RtInflateState, rt_zstd_st: mut RtZstdState, data: Xml, count: int) -> Val { + return tiled_decode_enc(rt_inflate_st, rt_zstd_st, data, xml_attr(data, "encoding"), xml_attr(data, "compression"), count) } # flatten a chunked (infinite-map) `` — its `` # children — into a dense GID list; sets `o`'s width/height/data (#74). -function tiled_chunked_layer(data: Xml, o: Val) -> void { +function tiled_chunked_layer(rt_inflate_st: mut RtInflateState, rt_zstd_st: mut RtZstdState, data: Xml, o: Val) -> void { let enc = xml_attr(data, "encoding") let comp = xml_attr(data, "compression") # pass 1: bounds over every chunk (tile coordinates) @@ -112,7 +112,7 @@ function tiled_chunked_layer(data: Xml, o: Val) -> void { let cy = xml_attr_int(ch, "y", 0) - miny let cw = xml_attr_int(ch, "width", 0) let cht = xml_attr_int(ch, "height", 0) - let cdata = tiled_decode_enc(ch, enc, comp, cw * cht) + let cdata = tiled_decode_enc(rt_inflate_st, rt_zstd_st, ch, enc, comp, cw * cht) var yy = 0 while yy < cht { var xx = 0 @@ -340,7 +340,7 @@ function tmx_layer_common(o: Val, el: Xml) -> void { if len(props.kids) > 0 { value_put(o, "properties", props) } } -function tmx_tilelayer_to_value(el: Xml, mapw: int, maph: int) -> Val { +function tmx_tilelayer_to_value(rt_inflate_st: mut RtInflateState, rt_zstd_st: mut RtZstdState, el: Xml, mapw: int, maph: int) -> Val { let o = value_object() value_put(o, "type", value_str("tilelayer")) tmx_layer_common(o, el) @@ -348,11 +348,11 @@ function tmx_tilelayer_to_value(el: Xml, mapw: int, maph: int) -> Val { let h = xml_attr_int(el, "height", maph) let data = xml_find(el, "data") if xml_count(data, "chunk") > 0 { # infinite map: flatten the chunks - tiled_chunked_layer(data, o) + tiled_chunked_layer(rt_inflate_st, rt_zstd_st, data, o) } else { value_put(o, "width", value_int(w)) value_put(o, "height", value_int(h)) - value_put(o, "data", tiled_data_list(data, w * h)) + value_put(o, "data", tiled_data_list(rt_inflate_st, rt_zstd_st, data, w * h)) } return o } @@ -374,7 +374,7 @@ function tmx_objectlayer_to_value(el: Xml) -> Val { # ---- map ------------------------------------------------------------------- # a `` root -> the intermediate map Value tree (Tiled JSON schema). -function tmx_to_value(root: Xml) -> Val { +function tmx_to_value(rt_inflate_st: mut RtInflateState, rt_zstd_st: mut RtZstdState, root: Xml) -> Val { let m = value_object() value_put(m, "type", value_str("map")) value_put(m, "version", value_str(xml_attr(root, "version"))) @@ -398,10 +398,10 @@ function tmx_to_value(root: Xml) -> Val { let ch = xml_child(root, i) let tag = xml_tag(ch) if tag == "tileset" { push(tilesets.kids, tmx_tileset_to_value(ch)) } - else { if tag == "layer" { push(layers.kids, tmx_tilelayer_to_value(ch, mapw, maph)) } + else { if tag == "layer" { push(layers.kids, tmx_tilelayer_to_value(rt_inflate_st, rt_zstd_st, ch, mapw, maph)) } else { if tag == "objectgroup" { push(layers.kids, tmx_objectlayer_to_value(ch)) } else { if tag == "imagelayer" { push(layers.kids, tmx_imagelayer_to_value(ch)) } - else { if tag == "group" { push(layers.kids, tmx_group_to_value(ch, mapw, maph)) } } } } } + else { if tag == "group" { push(layers.kids, tmx_group_to_value(rt_inflate_st, rt_zstd_st, ch, mapw, maph)) } } } } } i += 1 } let props = tmx_props_list(root) @@ -424,7 +424,7 @@ function tmx_imagelayer_to_value(el: Xml) -> Val { } # `` -> a Value object carrying its nested layers (P5 renders recursively). -function tmx_group_to_value(el: Xml, mapw: int, maph: int) -> Val { +function tmx_group_to_value(rt_inflate_st: mut RtInflateState, rt_zstd_st: mut RtZstdState, el: Xml, mapw: int, maph: int) -> Val { let o = value_object() value_put(o, "type", value_str("group")) tmx_layer_common(o, el) @@ -433,10 +433,10 @@ function tmx_group_to_value(el: Xml, mapw: int, maph: int) -> Val { while i < xml_child_count(el) { let ch = xml_child(el, i) let tag = xml_tag(ch) - if tag == "layer" { push(layers.kids, tmx_tilelayer_to_value(ch, mapw, maph)) } + if tag == "layer" { push(layers.kids, tmx_tilelayer_to_value(rt_inflate_st, rt_zstd_st, ch, mapw, maph)) } else { if tag == "objectgroup" { push(layers.kids, tmx_objectlayer_to_value(ch)) } else { if tag == "imagelayer" { push(layers.kids, tmx_imagelayer_to_value(ch)) } - else { if tag == "group" { push(layers.kids, tmx_group_to_value(ch, mapw, maph)) } } } } + else { if tag == "group" { push(layers.kids, tmx_group_to_value(rt_inflate_st, rt_zstd_st, ch, mapw, maph)) } } } } i += 1 } value_put(o, "layers", layers) @@ -447,13 +447,13 @@ function tmx_group_to_value(el: Xml, mapw: int, maph: int) -> Val { # The JSON reader already yields a Value tree; normalise it so it matches the XML # path: decode any base64 `data` string into a dense GID int list, in place, for # every tile layer (recursing into groups). -function tmj_normalize_layer(layer: Val) -> void { +function tmj_normalize_layer(rt_inflate_st: mut RtInflateState, rt_zstd_st: mut RtZstdState, layer: Val) -> void { if value_kind(layer) != 6 { return } let ty = value_as_str(value_get(layer, "type")) if ty == "group" { let ls = value_get(layer, "layers") var i = 0 - while i < value_count(ls) { tmj_normalize_layer(value_at(ls, i)); i += 1 } + while i < value_count(ls) { tmj_normalize_layer(rt_inflate_st, rt_zstd_st, value_at(ls, i)); i += 1 } return } if ty != "tilelayer" { return } @@ -462,26 +462,26 @@ function tmj_normalize_layer(layer: Val) -> void { # infinite map: flatten the JSON `chunks` array into a dense data list (#74) let chunks = value_get(layer, "chunks") if value_kind(chunks) == 5 and value_count(chunks) > 0 { - tmj_flatten_chunks(layer, chunks, enc, comp) + tmj_flatten_chunks(rt_inflate_st, rt_zstd_st, layer, chunks, enc, comp) return } let data = value_get(layer, "data") if value_kind(data) == 4 { # a base64 string let w = value_as_int(value_get(layer, "width")) let h = value_as_int(value_get(layer, "height")) - if enc == "base64" { value_put(layer, "data", tiled_b64_list(value_as_str(data), comp, w * h)) } + if enc == "base64" { value_put(layer, "data", tiled_b64_list(rt_inflate_st, rt_zstd_st, value_as_str(data), comp, w * h)) } } } # a JSON chunk's `data` (int array, or a base64 string) -> a GID Value list. -function tmj_chunk_gids(chunk: Val, enc: pointer, comp: pointer, count: int) -> Val { +function tmj_chunk_gids(rt_inflate_st: mut RtInflateState, rt_zstd_st: mut RtZstdState, chunk: Val, enc: pointer, comp: pointer, count: int) -> Val { let d = value_get(chunk, "data") - if value_kind(d) == 4 { return tiled_b64_list(value_as_str(d), comp, count) } # base64 string + if value_kind(d) == 4 { return tiled_b64_list(rt_inflate_st, rt_zstd_st, value_as_str(d), comp, count) } # base64 string return d # already an int array } # flatten JSON `chunks[]` into a dense data list on `layer`, sizing to the union. -function tmj_flatten_chunks(layer: Val, chunks: Val, enc: pointer, comp: pointer) -> void { +function tmj_flatten_chunks(rt_inflate_st: mut RtInflateState, rt_zstd_st: mut RtZstdState, layer: Val, chunks: Val, enc: pointer, comp: pointer) -> void { var minx = 1000000000 var miny = 1000000000 var maxx = -1000000000 @@ -509,7 +509,7 @@ function tmj_flatten_chunks(layer: Val, chunks: Val, enc: pointer, comp: pointer let cy = value_as_int(value_get(c, "y")) - miny let cw = value_as_int(value_get(c, "width")) let cht = value_as_int(value_get(c, "height")) - let cdata = tmj_chunk_gids(c, enc, comp, cw * cht) + let cdata = tmj_chunk_gids(rt_inflate_st, rt_zstd_st, c, enc, comp, cw * cht) var yy = 0 while yy < cht { var xx = 0 @@ -526,10 +526,10 @@ function tmj_flatten_chunks(layer: Val, chunks: Val, enc: pointer, comp: pointer value_put(layer, "data", gids) } -function tmj_normalize(m: Val) -> Val { +function tmj_normalize(rt_inflate_st: mut RtInflateState, rt_zstd_st: mut RtZstdState, m: Val) -> Val { let ls = value_get(m, "layers") var i = 0 - while i < value_count(ls) { tmj_normalize_layer(value_at(ls, i)); i += 1 } + while i < value_count(ls) { tmj_normalize_layer(rt_inflate_st, rt_zstd_st, value_at(ls, i)); i += 1 } return m } @@ -547,22 +547,22 @@ function tiled_first_byte(s: pointer) -> int { # read a map file (TMX or TMJ, auto-detected by first byte) -> the normalised # intermediate map Value tree. -function tiled_read(path: pointer) -> Val { +function tiled_read(rt_inflate_st: mut RtInflateState, rt_xml_st: mut RtXmlState, rt_zstd_st: mut RtZstdState, path: pointer) -> Val { let text = Fs.read_text(path) if text == null { return value_null() } if text == "" { return value_null() } if tiled_first_byte(text) == 60 { # '<' -> XML - return tmx_to_value(xml_parse(text)) + return tmx_to_value(rt_inflate_st, rt_zstd_st, xml_parse(rt_xml_st, text)) } - return tmj_normalize(json_parse(text)) # '{' -> JSON + return tmj_normalize(rt_inflate_st, rt_zstd_st, json_parse(text)) # '{' -> JSON } # read a tileset file (TSX or TSJ) -> a tileset Value object. -function tiled_read_tsx(path: pointer) -> Val { +function tiled_read_tsx(rt_xml_st: mut RtXmlState, path: pointer) -> Val { let text = Fs.read_text(path) if text == null { return value_null() } if text == "" { return value_null() } - if tiled_first_byte(text) == 60 { return tsx_to_value(xml_parse(text)) } + if tiled_first_byte(text) == 60 { return tsx_to_value(xml_parse(rt_xml_st, text)) } return json_parse(text) } @@ -734,9 +734,9 @@ function tmap_collision_kind(m: Tmap, gid: int) -> int { # as solid — the fallback source); false to drive collision from per-tile # metadata alone (objectgroup shapes / property convention on a visual layer). # solid -> '#' (35), one-way -> '=' (61), trigger/empty -> ' ' (32, passable). -function tmap_project_layer(m: Tmap, layer: int, whole_layer_solid: int) -> void { +function tmap_project_layer(rt_core_st: mut RtCoreState, m: Tmap, layer: int, whole_layer_solid: int) -> void { m.coll = layer - rt_map_size(m.w, m.h) # clamps to 96x64, clears to ' ' + rt_map_size(rt_core_st, m.w, m.h) # clamps to 96x64, clears to ' ' if layer < 0 or layer >= len(m.layers) { return } let l = m.layers[layer] if l.kind != 0 { return } @@ -750,8 +750,8 @@ function tmap_project_layer(m: Tmap, layer: int, whole_layer_solid: int) -> void if gid != 0 { var k = tmap_collision_kind(m, gid) if k == 0 and whole_layer_solid != 0 { k = 1 } # collision-layer fallback - if k == 1 { rt_map[y * 96 + x] = '#' } # '#' - if k == 2 { rt_map[y * 96 + x] = '=' } # '=' + if k == 1 { rt_core_st.rt_map[y * 96 + x] = '#' } # '#' + if k == 2 { rt_core_st.rt_map[y * 96 + x] = '=' } # '=' } } x += 1 @@ -763,11 +763,11 @@ function tmap_project_layer(m: Tmap, layer: int, whole_layer_solid: int) -> void # project a designated collision layer (non-zero GID is solid unless its tile # metadata says otherwise) — the P1 default, called automatically on load. -function tmap_project(m: Tmap, layer: int) -> void { tmap_project_layer(m, layer, 1) } +function tmap_project(rt_core_st: mut RtCoreState, m: Tmap, layer: int) -> void { tmap_project_layer(rt_core_st, m, layer, 1) } # drive collision from per-tile metadata alone (objectgroup hitboxes / property # convention) over any layer — a tile with no collision metadata stays passable. -function tmap_collide(m: Tmap, layer: int) -> void { tmap_project_layer(m, layer, 0) } +function tmap_collide(rt_core_st: mut RtCoreState, m: Tmap, layer: int) -> void { tmap_project_layer(rt_core_st, m, layer, 0) } # find a tile layer named collision/solids/walls (case-sensitive), or -1. function tmap_find_collision(m: Tmap) -> int { @@ -784,10 +784,10 @@ function tmap_find_collision(m: Tmap) -> int { # blit one tilew x tileh tile from a tileset image (source origin sx,sy) to the # framebuffer at (dx,dy), applying the three flip flags. Square tiles assumed # for the diagonal flip (Kenney art is 16x16), which is the orthogonal case. -function tmap_blit_tile(imgid: int, sx: int, sy: int, tw: int, th: int, dx: int, dy: int, fh: int, fv: int, fd: int) -> void { +function tmap_blit_tile(rt_core_st: mut RtCoreState, rt_image_st: RtImageState, imgid: int, sx: int, sy: int, tw: int, th: int, dx: int, dy: int, fh: int, fv: int, fd: int) -> void { if imgid < 0 { return } - let s: words = img_px[imgid] - let iw = img_w[imgid] + let s: words = rt_image_st.img_px[imgid] + let iw = rt_image_st.img_w[imgid] var j = 0 while j < th { var i = 0 @@ -798,7 +798,7 @@ function tmap_blit_tile(imgid: int, sx: int, sy: int, tw: int, th: int, dx: int, if fh == 1 { u = tw - 1 - u } if fv == 1 { v = th - 1 - v } let argb = s[(sy + v) * iw + (sx + u)] - rt_blend_px(dx + i, dy + j, argb) + rt_blend_px(rt_core_st, dx + i, dy + j, argb) i += 1 } j += 1 @@ -806,7 +806,7 @@ function tmap_blit_tile(imgid: int, sx: int, sy: int, tw: int, th: int, dx: int, } # draw one tile GID at map cell (x,y) with the camera offset already applied. -function tmap_draw_gid(m: Tmap, gid: int, dx: int, dy: int) -> void { +function tmap_draw_gid(rt_core_st: mut RtCoreState, rt_image_st: RtImageState, m: Tmap, gid: int, dx: int, dy: int) -> void { if gid == 0 { return } let r = tmap_resolve(m, gid) if r.tileset < 0 { return } @@ -821,7 +821,7 @@ function tmap_draw_gid(m: Tmap, gid: int, dx: int, dy: int) -> void { # Tiled anchors a tile by its bottom-left, so a tile taller than the map cell # rises above the cell. let ddy = dy - (ts.tileh - m.tileh) - tmap_blit_tile(ts.imgid, sx, sy, ts.tilew, ts.tileh, dx, ddy, r.fh, r.fv, r.fd) + tmap_blit_tile(rt_core_st, rt_image_st, ts.imgid, sx, sy, ts.tilew, ts.tileh, dx, ddy, r.fh, r.fv, r.fd) } # (tmap_draw / tmap_draw_anim are defined in the P3 section below, over the shared @@ -859,7 +859,7 @@ function tiled_join(a: pointer, b: pointer) -> string { return Path.normalize(Pa # build the runtime map from an intermediate tree, resolving external tilesets # and loading tileset images relative to `basedir`. -function tmap_build(tree: Val, basedir: pointer) -> Tmap { +function tmap_build(rt_image_st: mut RtImageState, rt_inflate_st: mut RtInflateState, rt_xml_st: mut RtXmlState, tree: Val, basedir: pointer) -> Tmap { let m = new Tmap m.tree = tree m.layers = new []TmLayer @@ -878,14 +878,14 @@ function tmap_build(tree: Val, basedir: pointer) -> Tmap { let src = value_as_str(value_get(tv, "source")) if src != "" { # external .tsx/.tsj let tsxpath = tiled_join(basedir, src) - let ext = tiled_read_tsx(tsxpath) + let ext = tiled_read_tsx(rt_xml_st, tsxpath) value_put(ext, "firstgid", value_get(tv, "firstgid")) tv = ext imgdir = Path.dir(tsxpath) } let ts = tmap_tileset_from_value(tv) let img = value_as_str(value_get(tv, "image")) - if img != "" { ts.imgid = rt_image_load(tiled_join(imgdir, img)) } + if img != "" { ts.imgid = rt_image_load(rt_image_st, rt_inflate_st, tiled_join(imgdir, img)) } push(m.tilesets, ts) i += 1 } @@ -898,7 +898,7 @@ function tmap_build(tree: Val, basedir: pointer) -> Tmap { while i < len(m.layers) { if m.layers[i].kind == 2 { let img = value_as_str(value_get(m.layers[i].data, "image")) - if img != "" { m.layers[i].imgid = rt_image_load(tiled_join(basedir, img)) } + if img != "" { m.layers[i].imgid = rt_image_load(rt_image_st, rt_inflate_st, tiled_join(basedir, img)) } } i += 1 } @@ -964,12 +964,12 @@ function tmap_add_layer(m: Tmap, lv: Val) -> void { # load a Tiled map file into the runtime model: read + parse (TMX or TMJ), build # the model (resolving external tilesets + images), and project the collision # layer (a tile layer named collision/solids/walls) down to the legacy tilemap. -function tiled_load(path: pointer) -> Tmap { - let tree = tiled_read(path) - let m = tmap_build(tree, Path.dir(path)) - tmap_resolve_templates(m, Path.dir(path)) # #72: fill template-instance objects +function tiled_load(rt_core_st: mut RtCoreState, rt_image_st: mut RtImageState, rt_inflate_st: mut RtInflateState, rt_xml_st: mut RtXmlState, rt_zstd_st: mut RtZstdState, path: pointer) -> Tmap { + let tree = tiled_read(rt_inflate_st, rt_xml_st, rt_zstd_st, path) + let m = tmap_build(rt_image_st, rt_inflate_st, rt_xml_st, tree, Path.dir(path)) + tmap_resolve_templates(rt_xml_st, m, Path.dir(path)) # #72: fill template-instance objects let c = tmap_find_collision(m) - if c >= 0 { tmap_project(m, c) } + if c >= 0 { tmap_project(rt_core_st, m, c) } return m } @@ -1032,7 +1032,7 @@ function tmap_is_animated(m: Tmap, gid: int) -> int { # draw every visible tile layer (animated tiles resolved for `frame`) plus every # tile object on the object layers, in file order, offset by the camera. -function tmap_draw_full(m: Tmap, camx: int, camy: int, frame: int, animate: int) -> void { +function tmap_draw_full(rt_core_st: mut RtCoreState, rt_image_st: RtImageState, m: Tmap, camx: int, camy: int, frame: int, animate: int) -> void { var li = 0 while li < len(m.layers) { let l = m.layers[li] @@ -1047,7 +1047,7 @@ function tmap_draw_full(m: Tmap, camx: int, camy: int, frame: int, animate: int) if gid != 0 { if animate != 0 { gid = tmap_frame_gid(m, gid, frame) } # orientation transform places the cell (orthogonal / iso / hex / staggered) - tmap_draw_gid(m, gid, tmap_cell_sx(m, x, y) + lox - camx, tmap_cell_sy(m, x, y) + loy - camy) + tmap_draw_gid(rt_core_st, rt_image_st, m, gid, tmap_cell_sx(m, x, y) + lox - camx, tmap_cell_sy(m, x, y) + loy - camy) } x += 1 } @@ -1055,7 +1055,7 @@ function tmap_draw_full(m: Tmap, camx: int, camy: int, frame: int, animate: int) } } if l.kind == 2 and l.visible != 0 { # image layer (parallax / repeat) - tmap_draw_imagelayer(m, l, camx, camy) + tmap_draw_imagelayer(rt_core_st, rt_image_st, m, l, camx, camy) } if l.kind == 1 and l.visible != 0 { # object layer: draw tile objects let objs = value_get(l.data, "objects") @@ -1068,7 +1068,7 @@ function tmap_draw_full(m: Tmap, camx: int, camy: int, frame: int, animate: int) let ox = value_as_int(value_get(ob, "x")) let oy = value_as_int(value_get(ob, "y")) # Tiled anchors a tile object by its bottom-left corner - tmap_draw_gid(m, g, ox - camx, oy - m.tileh - camy) + tmap_draw_gid(rt_core_st, rt_image_st, m, g, ox - camx, oy - m.tileh - camy) } oi += 1 } @@ -1078,11 +1078,11 @@ function tmap_draw_full(m: Tmap, camx: int, camy: int, frame: int, animate: int) } # static draw (no animation) — the P1 entry point. -function tmap_draw(m: Tmap, camx: int, camy: int) -> void { tmap_draw_full(m, camx, camy, 0, 0) } +function tmap_draw(rt_core_st: mut RtCoreState, rt_image_st: RtImageState, m: Tmap, camx: int, camy: int) -> void { tmap_draw_full(rt_core_st, rt_image_st, m, camx, camy, 0, 0) } # animated draw at engine `frame` (pass Time.frame): animated tiles advance, # deterministically and frame-identically across runs. -function tmap_draw_anim(m: Tmap, camx: int, camy: int, frame: int) -> void { tmap_draw_full(m, camx, camy, frame, 1) } +function tmap_draw_anim(rt_core_st: mut RtCoreState, rt_image_st: RtImageState, m: Tmap, camx: int, camy: int, frame: int) -> void { tmap_draw_full(rt_core_st, rt_image_st, m, camx, camy, frame, 1) } # ============================================================================ # P4 (#72) — object layers (shapes + text), custom properties/types, templates, @@ -1132,13 +1132,13 @@ function tiled_prop_node(container: Val, name: pointer) -> Val { # a property's raw string value, with a fallback to the object's custom-type # default (objecttypes.xml, via the type table). "" when absent everywhere. -function tiled_prop_str(container: Val, name: pointer) -> string { +function tiled_prop_str(rt_tiled_st: mut RtTiledState, container: Val, name: pointer) -> string { let p = tiled_prop_node(container, name) if value_kind(p) == 6 { return value_as_str(value_get(p, "value")) } # fall back to the container's class default let cls = value_as_str(value_get(container, "type")) if cls != "" { - let d = tiled_type_default(cls, name) + let d = tiled_type_default(rt_tiled_st, cls, name) if value_kind(d) == 6 { return value_as_str(value_get(d, "value")) } } return "" @@ -1146,19 +1146,19 @@ function tiled_prop_str(container: Val, name: pointer) -> string { # a property parsed as an integer ("true"/"false" -> 1/0), with the same default # fallback. -function tiled_prop_int(container: Val, name: pointer) -> int { - let s = tiled_prop_str(container, name) +function tiled_prop_int(rt_tiled_st: mut RtTiledState, container: Val, name: pointer) -> int { + let s = tiled_prop_str(rt_tiled_st, container, name) if s == "true" { return 1 } if s == "false" { return 0 } return xml_atoi(s) } -function tiled_prop_type(container: Val, name: pointer) -> string { +function tiled_prop_type(rt_tiled_st: mut RtTiledState, container: Val, name: pointer) -> string { let p = tiled_prop_node(container, name) if value_kind(p) == 6 { return value_as_str(value_get(p, "type")) } let cls = value_as_str(value_get(container, "type")) if cls != "" { - let d = tiled_type_default(cls, name) + let d = tiled_type_default(rt_tiled_st, cls, name) if value_kind(d) == 6 { return value_as_str(value_get(d, "type")) } } return "" @@ -1168,21 +1168,23 @@ function tiled_prop_type(container: Val, name: pointer) -> string { # The project custom-type table: an object mapping a type/class name to its list # of {name,type,value(default)} property definitions. class/enum properties then # resolve their defaults against it. -var tiled_type_table: Val = null +export state RtTiledState { + tiled_type_table: Val = null +} -function tiled_types() -> Val { - if tiled_type_table == null { tiled_type_table = value_object() } - return tiled_type_table +function tiled_types(rt_tiled_st: mut RtTiledState) -> Val { + if rt_tiled_st.tiled_type_table == null { rt_tiled_st.tiled_type_table = value_object() } + return rt_tiled_st.tiled_type_table } # load an objecttypes.xml file into the type table. Each maps # to its list. -function tiled_load_types(path: pointer) -> int { +function tiled_load_types(rt_tiled_st: mut RtTiledState, rt_xml_st: mut RtXmlState, path: pointer) -> int { let text = Fs.read_text(path) if text == null { return 0 } if text == "" { return 0 } - let root = xml_parse(text) - let tbl = tiled_types() + let root = xml_parse(rt_xml_st, text) + let tbl = tiled_types(rt_tiled_st) var n = 0 var i = 0 while i < xml_child_count(root) { @@ -1212,8 +1214,8 @@ function tiled_load_types(path: pointer) -> int { } # the default {name,type,value} property node for a custom type, or a null node. -function tiled_type_default(typename: pointer, propname: pointer) -> Val { - let tbl = tiled_types() +function tiled_type_default(rt_tiled_st: mut RtTiledState, typename: pointer, propname: pointer) -> Val { + let tbl = tiled_types(rt_tiled_st) let props = value_get(tbl, typename) if value_kind(props) != 5 { return value_null() } var i = 0 @@ -1228,12 +1230,12 @@ function tiled_type_default(typename: pointer, propname: pointer) -> Val { # ---- templates (.tx / .tj) ------------------------------------------------- # read a template file -> its object Value (the a .tx wraps, or the # "object" of a .tj). External reusable object definitions. -function tiled_read_template(path: pointer) -> Val { +function tiled_read_template(rt_xml_st: mut RtXmlState, path: pointer) -> Val { let text = Fs.read_text(path) if text == null { return value_null() } if text == "" { return value_null() } if tiled_first_byte(text) == 60 { # XML .tx - let root = xml_parse(text) #