diff --git a/docs/WM6_IPAQ_212.md b/docs/WM6_IPAQ_212.md new file mode 100644 index 000000000..b5945efef --- /dev/null +++ b/docs/WM6_IPAQ_212.md @@ -0,0 +1,182 @@ +# HP iPAQ 212 / Windows Mobile 6 port + +## Decision + +PocketJS should reach the iPAQ 212 through a native Windows Mobile host, built +by Visual C++ 2005 for the Windows Mobile 6 Professional SDK's ARMV4I target. +The repository now contains a first, deployable solution at +`hosts/wm6/vs2005/PocketJS.WM6.sln`. + +This is intentionally an experimental port, not a production target in +`POCKET_TARGETS`. Adding a target profile before the complete guest runtime and +its capabilities work on hardware would make build admission promise behavior +that the host does not deliver. + +## Why this target + +The HP iPAQ 212 belongs to the iPAQ 210 series. It has a 624 MHz Marvell +PXA310, 128 MB SDRAM, and a 4-inch 480×640 touch display, and ships with +Windows Mobile 6 Classic. Despite the device name, Microsoft's SDK mapping is: + +| Device class | Required WM6 SDK | +| --- | --- | +| Classic / Pocket PC | Professional SDK | +| Professional / Pocket PC Phone | Professional SDK | +| Standard / Smartphone | Standard SDK | + +The Visual Studio platform must therefore read +`Windows Mobile 6 Professional SDK (ARMV4I)`. ARMV4I is the SDK ABI and is +compatible with the PXA310; selecting an unofficial CPU-specific target would +make the binary less portable and would bypass the SDK libraries. + +## Honest feasibility boundary + +Creating a `.sln` is necessary but not sufficient for the existing guest +runtime: + +- the native PocketJS hosts embed QuickJS; +- the retained renderer lives in the Rust `engine/core` crate; +- Visual C++ 2005 predates modern C/C++ language features; +- stable Rust has no Windows CE 5.2 / ARMV4I target; the implemented build + therefore emits ARMv4T ELF and performs a checked relocation/symbol + conversion to WinCE COFF with dual-target CeGCC binutils; +- Windows Mobile 6 has neither a suitable browser runtime nor modern Web APIs, + so wrapping the web host is not a viable shortcut. + +The lowest-risk route is to validate each irreversible assumption on the real +device before porting the next layer. + +## Port gates + +### Gate 0 — native hardware probe (implemented) + +Build and run `hosts/wm6/vs2005/PocketJS.WM6.sln`. Accept this gate only when +the physical iPAQ shows: + +- `screen: 480 x 640` in portrait (or `640 x 480` after rotation); +- a steadily increasing frame count near the 33 ms timer interval; +- touch coordinates and DOWN/up state following the stylus; +- distinct hexadecimal key codes for the D-pad and centre button; +- stable free RAM after leaving the probe running for ten minutes. + +Record the ROM version, orientation, displayed memory values, D-pad key codes, +and whether the Back/Escape key exits. Emulator-only success is insufficient. + +Windows Mobile virtualizes a VGA device as `240×320` for legacy applications. +The probe embeds `HI_RES_AWARE CEUX { 1 }` to opt out of that compatibility +scaling. A QVGA result therefore means an older or incorrectly linked resource +was deployed; it is not the iPAQ panel's physical resolution. + +### Gate 1 — toolchain-owned QuickJS probe (first executable implemented) + +Current QuickJS cannot be built by VC8. The implemented split toolchain keeps +VS2005 as the WM6 window/deployment/debug host and builds QuickJS with CeGCC's +native-API `arm-mingw32ce` compiler. The checked-in ARM/WinCE probe now proves: + +1. runtime/context creation; +2. evaluation of an embedded UTF-8 script; +3. a native `print` callback plus Promise pending-job draining; +4. explicit 8 MiB memory and 256 KiB stack limits; +5. 100 repeated create/evaluate/drain/destroy cycles. + +Its pinned source, compatibility patch, and reproducible build command live in +`hosts/wm6/quickjs`. `PocketJS.WM6.QuickJS` in the VS2005 solution deploys the +resulting CeGCC executable and starts it on the selected device or emulator. +The CeGCC build targets ARMV4T with interworking rather than the PXA310's +ARMv5TE extensions: the WM6 ARMV4I emulator rejects ARMv5-only instructions +such as `CLZ`, while the physical PXA310 remains backward-compatible. + +QuickJS now has a dedicated WinCE compatibility layer for allocation, missing +CRT calls, and the subset used by the Hero guest. Those changes remain a +reviewable patch, as the Symbian toolchain does. Gate 1 remains open until the 100-cycle +probe passes on physical iPAQ hardware with before/after free-memory receipts; +emulator success validates the binary and ABI but cannot close the hardware +gate. + +The repository-pinned QuickJS revision cannot be compiled directly by VC8: it +uses C99 syntax, flexible arrays, GCC builtins/attributes, compound literals, +and designated initializers. Compiling it as C++ is also not a shortcut because +the C sources rely on implicit `void *` conversions and C linkage rules. Gate 1 +therefore uses a separately owned GNU WinCE build. The future VS2005 host will +load a CeGCC-built DLL through a narrow C ABI; QuickJS values and allocator +ownership must never cross that boundary. That DLL boundary is now +implemented as ABI v3; it also owns the linked Rust core so QuickJS values, +Rust allocations, and framebuffer pointers stay on the CeGCC side. + +### AOT milestone — Pocket Vapor Todo (implemented) + +`PocketJS.WM6.Vapor` is an independent application project in the same VS2005 +solution. It links: + +- a checked-in copy of the target-independent `vapor/runtime/vapor_core.c` + (kept inside the solution so a `Y:\vs2005` VM mount is self-contained); +- deterministic C generated from `vapor/examples/todo/todo.tsx`; +- a WM6 GDI cell-grid host with D-pad, centre, Back, soft-key, and stylus + mappings. + +This milestone deliberately follows the AOT-first option described below. It +proves that Pocket-authored reactive UI can execute on the SDK ABI without a +heap or JavaScript engine, and it gives the real iPAQ a useful memory/input +test while QuickJS compatibility work continues. It must not be described as +ordinary PocketJS guest compatibility. + +### Gate 2 — native Rust renderer (implemented; runtime receipt pending) + +The guest-compatible path now reuses the actual Rust core rather than +rewriting it in C: + +1. nightly rustc builds the freestanding core for `armv4t-none-eabi`; +2. ARM ELF ld merges per-item sections and discards unwind/LLVM metadata; +3. dual-target CeGCC binutils converts ELF to WinCE COFF; +4. a repository tool reconstructs all COFF relocation symbol indices and + strictly maps `R_ARM_ABS32`, `R_ARM_CALL`, and `R_ARM_JUMP24`; +5. CeGCC links that object into the QuickJS DLL. + +The final DLL has 16 PE sections instead of thousands of Rust per-item +sections. The core renders its real incremental ARGB32 framebuffer at the +rotated native viewport; the VC8 host converts it to an RGB565 staging buffer +and presents it through DirectDraw. The next emulator run must provide the +runtime receipt before this gate is considered closed. + +### Gate 3 — PocketJS HostOps (implemented; runtime receipt pending) + +The ABI v3 DLL installs native lifecycle, node, style/property batch, text, +texture/font, animation, focus/hit-test, debug, tick, and render operations. +It copies the PAK into QuickJS before evaluating the unmodified Hero bundle, +calls `globalThis.frame` at the WM6 timer cadence, ticks the core, and returns +the incremental framebuffer. The former JavaScript tree and hand-authored +draw-list adapter have been removed. D-pad keys and Enter/Space currently feed +the PocketJS directional/Circle button bits. Stylus down/move/up snapshots use +the wide 10-bit PocketJS touch wire format so VGA coordinates are not +truncated. + +### Gate 4 — packaging and production admission + +Produce a CAB only after direct EXE deployment is stable. The CAB should +install one application under `\Program Files\PocketJS`, add a Start Menu +shortcut, and uninstall without touching shared storage. Promote WM6 to +`POCKET_TARGETS` only when: + +- the stock host boots a real `.pocket` guest; +- viewport and input contracts have device tests; +- memory has a measured ceiling; +- suspend/resume and orientation behavior are defined; +- an iPAQ 212 hardware receipt identifies the ROM and binary hash. + +## Recommended build machine + +Use an isolated 32-bit Windows XP SP3 VM for the shortest path, with Visual +Studio 2005 SP1, the Smart Device C++ feature, and the WM6 Professional SDK +Refresh. ActiveSync 4.5 is the period-correct XP deployment path. A Vista VM +can use Windows Mobile Device Center plus Microsoft's VS2005 Vista updates, +but modern Windows hosts add driver and installer failure modes unrelated to +the port. + +Keep the VM offline except while obtaining original toolchain installers. +Never flash the iPAQ as part of this workflow; copying or debugging an +application does not require a ROM change. + +## Sources + +- [Microsoft: Windows Mobile 6 SDK Refresh download and SDK mapping](https://www.microsoft.com/en-us/download/details.aspx?id=6135) +- [HP: iPAQ 200 series product specifications](https://support.hp.com/tw-zh/document/c01419121) diff --git a/engine/symbian/Cargo.toml b/engine/symbian/Cargo.toml index ea5591fa1..b76bf4c11 100644 --- a/engine/symbian/Cargo.toml +++ b/engine/symbian/Cargo.toml @@ -22,8 +22,10 @@ edition = "2021" crate-type = ["rlib", "staticlib"] [features] -default = ["standalone-extension-provider"] +default = ["standalone-extension-provider", "freestanding", "gles2"] standalone-extension-provider = [] +freestanding = [] +gles2 = [] [dependencies] pocketjs-core = { path = "../core" } diff --git a/engine/symbian/src/lib.rs b/engine/symbian/src/lib.rs index 9431c2979..6b3275b7f 100644 --- a/engine/symbian/src/lib.rs +++ b/engine/symbian/src/lib.rs @@ -11,36 +11,36 @@ //! packed, top-left-origin ARGB32 pixels; those pointers remain valid until //! the next capture, viewport change, init, or shutdown call. -#![cfg_attr(target_os = "none", no_std)] -#![cfg_attr(target_os = "none", feature(alloc_error_handler))] +#![cfg_attr(all(feature = "freestanding", not(test)), no_std)] +#![cfg_attr(all(feature = "freestanding", not(test)), feature(alloc_error_handler))] #![allow(static_mut_refs)] #![allow(clippy::not_unsafe_ptr_arg_deref)] extern crate alloc; use alloc::vec::Vec; -#[cfg(target_os = "none")] +#[cfg(all(feature = "freestanding", not(test)))] use core::alloc::{GlobalAlloc, Layout}; -#[cfg(target_os = "none")] +#[cfg(all(feature = "freestanding", not(test)))] use core::ffi::c_void; use pocketjs_core::damage::{DamagePolicy, DamageTracker, DEFAULT_DAMAGE_REGIONS}; use pocketjs_core::raster; use pocketjs_core::Ui; -#[cfg(any(target_os = "none", test))] -mod gles2; pub mod extension; +#[cfg(feature = "gles2")] +mod gles2; -#[cfg(any(target_os = "none", test))] +#[cfg(any(feature = "freestanding", test))] const C_MALLOC_ALIGNMENT: usize = 8; -#[cfg(any(target_os = "none", test))] +#[cfg(any(feature = "freestanding", test))] #[inline] const fn c_allocator_supports_alignment(alignment: usize) -> bool { alignment <= C_MALLOC_ALIGNMENT } -#[cfg(target_os = "none")] +#[cfg(all(feature = "freestanding", not(test)))] unsafe extern "C" { fn malloc(size: usize) -> *mut c_void; fn realloc(ptr: *mut c_void, size: usize) -> *mut c_void; @@ -48,10 +48,10 @@ unsafe extern "C" { fn abort() -> !; } -#[cfg(target_os = "none")] +#[cfg(all(feature = "freestanding", not(test)))] struct CAllocator; -#[cfg(target_os = "none")] +#[cfg(all(feature = "freestanding", not(test)))] unsafe impl GlobalAlloc for CAllocator { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { if !c_allocator_supports_alignment(layout.align()) { @@ -72,17 +72,17 @@ unsafe impl GlobalAlloc for CAllocator { } } -#[cfg(target_os = "none")] +#[cfg(all(feature = "freestanding", not(test)))] #[global_allocator] static ALLOCATOR: CAllocator = CAllocator; -#[cfg(target_os = "none")] +#[cfg(all(feature = "freestanding", not(test)))] #[panic_handler] fn panic(_info: &core::panic::PanicInfo<'_>) -> ! { unsafe { abort() } } -#[cfg(target_os = "none")] +#[cfg(all(feature = "freestanding", not(test)))] #[alloc_error_handler] fn allocation_error(_layout: Layout) -> ! { unsafe { abort() } @@ -145,7 +145,7 @@ fn clear_framebuffer() { /// Reset the single UI instance. `raster_density == 0` selects density 1. #[no_mangle] pub extern "C" fn ui_init(raster_density: u32) { - #[cfg(target_os = "none")] + #[cfg(feature = "gles2")] unsafe { // This call may happen without a current GL context, so the backend // only marks its caches stale and defers replacement until render. @@ -160,7 +160,7 @@ pub extern "C" fn ui_init(raster_density: u32) { /// Drop all retained UI, texture, font, and framebuffer allocations. #[no_mangle] pub extern "C" fn ui_shutdown() { - #[cfg(target_os = "none")] + #[cfg(feature = "gles2")] unsafe { gles2::invalidate_resources(); } @@ -365,7 +365,7 @@ pub extern "C" fn ui_load_styles(ptr: *const u8, len: usize) -> i32 { pub extern "C" fn ui_load_font_atlas(ptr: *const u8, len: usize) -> i32 { let blob = unsafe { bytes(ptr, len) }; let loaded = ui().load_font_atlas(blob); - #[cfg(target_os = "none")] + #[cfg(feature = "gles2")] if loaded { if let Some(&slot) = blob.get(12) { unsafe { @@ -392,17 +392,17 @@ pub extern "C" fn ui_tick() { #[no_mangle] pub extern "C" fn ui_gl_initialize() -> i32 { - #[cfg(target_os = "none")] + #[cfg(feature = "gles2")] unsafe { return gles2::initialize() as i32; } - #[cfg(not(target_os = "none"))] + #[cfg(not(feature = "gles2"))] 0 } #[no_mangle] pub extern "C" fn ui_gl_reset_resources() { - #[cfg(target_os = "none")] + #[cfg(feature = "gles2")] unsafe { gles2::reset_resources(); } @@ -410,7 +410,7 @@ pub extern "C" fn ui_gl_reset_resources() { #[no_mangle] pub extern "C" fn ui_gl_shutdown() { - #[cfg(target_os = "none")] + #[cfg(feature = "gles2")] unsafe { gles2::shutdown(); } @@ -425,7 +425,7 @@ pub extern "C" fn ui_gl_render( window_width: i32, window_height: i32, ) -> i32 { - #[cfg(target_os = "none")] + #[cfg(feature = "gles2")] unsafe { return gles2::render( ui(), @@ -437,7 +437,7 @@ pub extern "C" fn ui_gl_render( window_height, ) as i32; } - #[cfg(not(target_os = "none"))] + #[cfg(not(feature = "gles2"))] { let _ = ( target_x, @@ -463,7 +463,7 @@ pub extern "C" fn ui_gl_render_over( window_width: i32, window_height: i32, ) -> i32 { - #[cfg(target_os = "none")] + #[cfg(feature = "gles2")] unsafe { return gles2::render_over( ui(), @@ -475,7 +475,7 @@ pub extern "C" fn ui_gl_render_over( window_height, ) as i32; } - #[cfg(not(target_os = "none"))] + #[cfg(not(feature = "gles2"))] { let _ = ( target_x, diff --git a/engine/wm6/README.md b/engine/wm6/README.md new file mode 100644 index 000000000..5398cffc1 --- /dev/null +++ b/engine/wm6/README.md @@ -0,0 +1,46 @@ +# PocketJS Core for Windows Mobile 6 + +This target reuses the complete retained `pocketjs-core` through the existing +Symbian C ABI. The two legacy hosts have the same useful boundary: a C/C++ +application owns its event loop and presentation surface, while a freestanding +Rust static library owns the tree, style table, layout, animation, DrawList, +font/image registries, and deterministic CPU rasterizer. + +Rust deliberately emits ordinary `armv4t-none-eabi` ELF first. CeGCC and +Visual Studio 2005 cannot consume that object directly, so `build-core.sh` +performs a narrow, checked conversion to `pe-arm-wince` COFF: + +1. rustc builds the complete core for the ARMv4T soft-float baseline; +2. GNU ARM ELF ld folds Rust's per-item sections into `.text`, `.rdata`, + `.data`, and `.bss`, and removes unwind/LLVM metadata; +3. a dual-target CeGCC binutils `objcopy` writes WinCE COFF; +4. `patch_arm_coff_relocs.py` maps the three emitted ARM relocations and + rebuilds every relocation symbol index from the authoritative ELF tables. + It also clears the ELF branch instruction's `-8` PC-bias addend because + WinCE `ARM_26` uses the CeGCC convention of a zero immediate; +5. WinCE ld performs a second relocatable link as a structural verification; +6. the build rejects a core that is not ARMv4T, does not contain exactly the + four folded COFF sections, loses a required `ui_*` entry point, or imports + anything beyond the four allocator/abort functions supplied by the host. + +The WM6 build disables the Symbian GLES2 backend. It uses `ui_render_incremental` +to obtain the real PocketJS ARGB32 framebuffer, then converts/presents that +buffer through the WM6 RGB565 DirectDraw layer. + +This requires an ordinary `arm-none-eabi` binutils installation and the +CeGCC 9.3 binutils configured with both `arm-mingw32ce` and +`arm-none-eabi` BFD targets. Point the script at those installed tools, or at +the corresponding build-tree executables: + +```sh +WM6_CE_OBJCOPY=/opt/cegcc/bin/arm-mingw32ce-objcopy \ +WM6_CE_OBJDUMP=/opt/cegcc/bin/arm-mingw32ce-objdump \ +WM6_CE_LD=/opt/cegcc/bin/arm-mingw32ce-ld \ +WM6_CE_NM=/opt/cegcc/bin/arm-mingw32ce-nm \ + bash engine/wm6/build-core.sh +``` + +The default output is +`hosts/wm6/vs2005/prebuilt/PocketJS.WM6.Core.obj`. The QuickJS runtime build +links that object into `PocketJS.WM6.QuickJS.v3.dll`; the standalone object is a +generated intermediate and is not deployed. diff --git a/engine/wm6/build-core.sh b/engine/wm6/build-core.sh new file mode 100755 index 000000000..e97d0febc --- /dev/null +++ b/engine/wm6/build-core.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "${script_dir}/../.." && pwd)" +output="${1:-${repo_root}/hosts/wm6/vs2005/prebuilt/PocketJS.WM6.Core.obj}" +work_dir="$(mktemp -d "${TMPDIR:-/tmp}/pocketjs-wm6-core.XXXXXX")" +trap 'rm -rf "$work_dir"' EXIT + +arm_ld="${ARM_NONE_EABI_LD:-arm-none-eabi-ld}" +arm_objcopy="${ARM_NONE_EABI_OBJCOPY:-arm-none-eabi-objcopy}" +arm_readelf="${ARM_NONE_EABI_READELF:-arm-none-eabi-readelf}" +ce_objcopy="${WM6_CE_OBJCOPY:-arm-mingw32ce-objcopy}" +ce_objdump="${WM6_CE_OBJDUMP:-arm-mingw32ce-objdump}" +ce_ld="${WM6_CE_LD:-arm-mingw32ce-ld}" +ce_nm="${WM6_CE_NM:-arm-mingw32ce-nm}" +cargo="${CARGO:-cargo}" + +for tool in \ + "$cargo" "$arm_ld" "$arm_objcopy" "$arm_readelf" \ + "$ce_objcopy" "$ce_objdump" "$ce_ld" "$ce_nm"; do + if ! command -v "$tool" >/dev/null 2>&1; then + echo "Required tool not found: ${tool}" >&2 + exit 2 + fi +done + +export CARGO_TARGET_DIR="${work_dir}/cargo" +RUSTUP_TOOLCHAIN="${RUSTUP_TOOLCHAIN:-nightly}" "$cargo" build \ + --manifest-path "${repo_root}/engine/symbian/Cargo.toml" \ + --release --no-default-features --features freestanding \ + --target armv4t-none-eabi \ + -Z build-std=core,alloc,compiler_builtins \ + -Z build-std-features=compiler-builtins-mem + +rust_archive="${CARGO_TARGET_DIR}/armv4t-none-eabi/release/libpocketjs_symbian_core.a" +aggregate="${work_dir}/pocketjs-core.elf.o" +localized="${work_dir}/pocketjs-core-localized.elf.o" +folded="${work_dir}/pocketjs-core-folded.elf.o" +unpatched="${work_dir}/pocketjs-core-unpatched.obj" +remapped="${work_dir}/pocketjs-core-remapped.obj" +linked="${work_dir}/PocketJS.WM6.Core.obj" + +"$arm_ld" -m armelf -r -o "$aggregate" \ + --whole-archive "$rust_archive" --no-whole-archive +if ! "$arm_readelf" -A "$aggregate" | + grep -Eq 'Tag_CPU_arch:[[:space:]]+v4T[[:space:]]*$'; then + echo "Rust core does not advertise the required ARMv4T baseline" >&2 + "$arm_readelf" -A "$aggregate" >&2 + exit 3 +fi +"$arm_objcopy" --wildcard \ + --keep-global-symbol='ui_*' \ + "$aggregate" "$localized" +"$arm_ld" -m armelf -r -x \ + -T "${script_dir}/core-sections.ld" \ + -o "$folded" "$localized" +"$ce_objcopy" \ + --strip-symbol=__aeabi_unwind_cpp_pr0 \ + -O pe-arm-wince-little \ + "$folded" "$unpatched" +python3 "${script_dir}/tools/patch_arm_coff_relocs.py" \ + "$folded" "$unpatched" "$remapped" +"$ce_ld" -m arm_wince_pe -r -o "$linked" "$remapped" + +for symbol in ui_init ui_create_node ui_load_styles ui_render_incremental; do + if ! "$ce_nm" "$linked" | grep -q " T ${symbol}\$"; then + echo "Converted core is missing required export: ${symbol}" >&2 + exit 3 + fi +done + +section_names="$( + "$ce_objdump" -h "$linked" | + awk '/^[[:space:]]*[0-9]+[[:space:]]+\./ { print $2 }' | + paste -sd ' ' - +)" +if [[ "$section_names" != ".text .data .rdata .bss" ]]; then + echo "Unexpected converted core sections: ${section_names}" >&2 + "$ce_objdump" -h "$linked" >&2 + exit 3 +fi + +undefined_symbols="$( + "$ce_nm" -u "$linked" | + awk '{ print $2 }' | + sort | + paste -sd ' ' - +)" +if [[ "$undefined_symbols" != "abort free malloc realloc" ]]; then + echo "Unexpected converted core imports: ${undefined_symbols}" >&2 + "$ce_nm" -u "$linked" >&2 + exit 3 +fi + +mkdir -p "$(dirname "$output")" +cp "$linked" "$output" +echo "Built ${output}" diff --git a/engine/wm6/core-sections.ld b/engine/wm6/core-sections.ld new file mode 100644 index 000000000..be39f8b02 --- /dev/null +++ b/engine/wm6/core-sections.ld @@ -0,0 +1,45 @@ +/* + * Rust and LLVM emit one section per function/data item. The old CeGCC PE + * linker treats unknown .text.* and .rodata.* inputs as separate output + * sections, creating thousands of 512-byte-aligned PE sections. Fold them + * while the object is still ELF, where GNU ld understands all ARM relocs. + */ +SECTIONS +{ + .text 0 : ALIGN(4) + { + *(.text) + *(.text.*) + } + /* WinCE PE names its read-only data section .rdata. */ + .rdata 0 : ALIGN(8) + { + *(.rodata) + *(.rodata.*) + } + .data 0 : ALIGN(8) + { + *(.data) + *(.data.*) + } + .bss 0 : ALIGN(8) + { + *(.bss) + *(.bss.*) + *(COMMON) + } + .noinit 0 : ALIGN(4) + { + *(.noinit) + *(.noinit.*) + } + /DISCARD/ : + { + *(.ARM.exidx*) + *(.comment) + *(.llvmbc) + *(.llvmcmd) + *(.note.GNU-stack) + *(.ARM.attributes) + } +} diff --git a/engine/wm6/tools/patch_arm_coff_relocs.py b/engine/wm6/tools/patch_arm_coff_relocs.py new file mode 100644 index 000000000..e94533c29 --- /dev/null +++ b/engine/wm6/tools/patch_arm_coff_relocs.py @@ -0,0 +1,789 @@ +#!/usr/bin/env python3 +"""Repair an ARM ELF object converted to WinCE COFF by BFD objcopy. + +This is intentionally narrow. The PocketJS WM6 core is first linked into one +ARMv4T ELF relocatable object and has all .ARM.exidx sections removed. At that +point rustc currently emits only: + + R_ARM_ABS32 (2) -> ARM_32 (1) + R_ARM_CALL (28) -> ARM_26 (3) + R_ARM_JUMP24 (29) -> ARM_26 (3) + +The CeGCC BFD objcopy can copy all sections from ELF to COFF, but its generic +format converter preserves the ELF relocation numbers *and* the ELF symbol +indices. The latter do not identify the corresponding COFF symbols. ELF ARM +branches also carry a -8 instruction addend for the architecture's PC bias, +while WinCE ARM_26 expects a zero immediate and applies that bias itself. This +tool uses the source ELF relocation tables as the authority, appends exact +COFF symbols for their targets, normalizes branch addends, rewrites every +relocation, and rejects unexpected input instead of silently producing a +corrupt object. +""" + +from __future__ import annotations + +import argparse +import struct +from collections import Counter +from dataclasses import dataclass +from pathlib import Path + + +COFF_HEADER_SIZE = 20 +COFF_SECTION_SIZE = 40 +COFF_RELOCATION_SIZE = 10 +COFF_SYMBOL_SIZE = 18 +IMAGE_FILE_MACHINE_ARM = 0x01C0 +IMAGE_FILE_MACHINE_THUMB = 0x01C2 +IMAGE_SCN_LNK_NRELOC_OVFL = 0x01000000 +IMAGE_SYM_CLASS_EXTERNAL = 2 +IMAGE_SYM_CLASS_STATIC = 3 +IMAGE_REL_ARM_BRANCH24 = 3 + +ELF_HEADER_SIZE = 52 +ELF_SECTION_SIZE = 40 +ELF_SYMBOL_SIZE = 16 +ELF_RELOCATION_SIZE = 8 +EM_ARM = 40 +ET_REL = 1 +SHT_SYMTAB = 2 +SHT_REL = 9 +SHN_UNDEF = 0 +SHN_ABS = 0xFFF1 + +ELF_TO_WINCE_RELOCATION = { + 2: 1, # R_ARM_ABS32 -> ARM_32 + 28: IMAGE_REL_ARM_BRANCH24, # R_ARM_CALL -> ARM_26 + 29: IMAGE_REL_ARM_BRANCH24, # R_ARM_JUMP24 -> ARM_26 +} + + +class CoffError(RuntimeError): + pass + + +@dataclass(frozen=True) +class ElfSection: + index: int + name: str + kind: int + offset: int + size: int + link: int + info: int + entry_size: int + + +@dataclass(frozen=True) +class ElfSymbol: + index: int + name: str + value: int + section_index: int + + +@dataclass(frozen=True) +class CoffSection: + index: int + name: str + header: int + data_offset: int + data_size: int + relocation_offset: int + relocation_count: int + + +def read_u16(data: bytes | bytearray, offset: int) -> int: + return struct.unpack_from(" int: + return struct.unpack_from(" None: + if offset < 0 or size < 0 or offset + size > len(data): + raise CoffError( + f"{label} lies outside the COFF object " + f"(offset={offset}, size={size}, file={len(data)})" + ) + + +def read_elf_string(data: bytes, table: ElfSection, offset: int, label: str) -> str: + if offset < 0 or offset >= table.size: + raise CoffError(f"{label} has invalid ELF string-table offset {offset}") + start = table.offset + offset + end = data.find(b"\0", start, table.offset + table.size) + if end < 0: + raise CoffError(f"{label} is not NUL-terminated") + try: + return data[start:end].decode("ascii") + except UnicodeDecodeError as error: + raise CoffError(f"{label} is not ASCII") from error + + +def parse_elf(data: bytes) -> tuple[list[ElfSection], list[ElfSymbol]]: + checked_range(data, 0, ELF_HEADER_SIZE, "ELF header") + if data[:6] != b"\x7fELF\x01\x01": + raise CoffError("expected a little-endian ELF32 input object") + elf_type, machine = struct.unpack_from("= section_count: + raise CoffError("extended ELF section numbering is not supported") + checked_range( + data, + section_table, + section_count * section_size, + "ELF section table", + ) + + raw_sections: list[tuple[int, ...]] = [] + for index in range(section_count): + raw_sections.append( + struct.unpack_from( + "= len(sections): + raise CoffError("ELF symbol table has an invalid string-table link") + string_table = sections[symbol_table.link] + if symbol_table.size % ELF_SYMBOL_SIZE: + raise CoffError("ELF symbol table has a partial final record") + + symbols: list[ElfSymbol] = [] + for index in range(symbol_table.size // ELF_SYMBOL_SIZE): + record = symbol_table.offset + index * ELF_SYMBOL_SIZE + name_offset, value, _size = struct.unpack_from(" str: + if offset < 0 or offset >= limit: + raise CoffError(f"{label} has invalid string-table offset {offset}") + end = data.find(b"\0", offset, limit) + if end < 0: + # GNU BFD permits the final COFF string to end exactly at EOF. + end = limit + return data[offset:end].decode("ascii") + + +def symbol_name( + data: bytearray, + record: int, + string_table: int, + string_table_end: int, +) -> str: + raw = data[record : record + 8] + if raw[:4] == b"\0\0\0\0": + offset = struct.unpack_from(" str: + raw = bytes(data[header : header + 8]).split(b"\0", 1)[0] + if raw.startswith(b"/"): + try: + offset = int(raw[1:].decode("ascii"), 10) + except ValueError as error: + raise CoffError(f"invalid long COFF section name {raw!r}") from error + return read_c_string( + data, + string_table + offset, + string_table_end, + "COFF section", + ) + return raw.decode("ascii") + + +def parse_coff_sections(data: bytearray) -> list[CoffSection]: + checked_range(data, 0, COFF_HEADER_SIZE, "COFF header") + machine, section_count = struct.unpack_from(" tuple[bytearray, list[int]]: + if not symbols: + return data, [] + + symbol_table = read_u32(data, 8) + symbol_count = read_u32(data, 12) + string_table = symbol_table + symbol_count * COFF_SYMBOL_SIZE + checked_range( + data, + symbol_table, + symbol_count * COFF_SYMBOL_SIZE, + "COFF symbol table", + ) + checked_range(data, string_table, 4, "COFF string table") + string_table_size = read_u32(data, string_table) + checked_range(data, string_table, string_table_size, "COFF string table") + string_table_end = string_table + string_table_size + + strings = bytearray(data[string_table:string_table_end]) + if len(strings) < 4: + raise CoffError("invalid COFF string table") + if strings[-1] != 0: + strings.append(0) + + string_offsets: dict[str, int] = {} + cursor = 4 + while cursor < len(strings): + end = strings.find(b"\0", cursor) + if end < 0: + end = len(strings) + if end > cursor: + string_offsets[bytes(strings[cursor:end]).decode("ascii")] = cursor + cursor = end + 1 + + records = bytearray() + indices: list[int] = [] + for name, value, section, storage_class in symbols: + try: + encoded = name.encode("ascii") + except UnicodeEncodeError as error: + raise CoffError(f"COFF symbol {name!r} is not ASCII") from error + if not encoded: + raise CoffError("cannot append an unnamed COFF symbol") + if len(encoded) <= 8: + name_field = encoded.ljust(8, b"\0") + else: + offset = string_offsets.get(name) + if offset is None: + offset = len(strings) + strings.extend(encoded) + strings.append(0) + string_offsets[name] = offset + name_field = b"\0\0\0\0" + struct.pack(" tuple[bytearray, Counter[tuple[int, int]], int, int]: + elf_sections, elf_symbols = parse_elf(elf_data) + coff_sections = parse_coff_sections(coff_data) + + coff_by_name: dict[str, CoffSection] = {} + for section in coff_sections: + if section.name in coff_by_name: + raise CoffError(f"duplicate COFF section name {section.name!r}") + coff_by_name[section.name] = section + + pending: list[tuple[int, int, int]] = [] + normalized_branches = 0 + matched_coff_sections: set[int] = set() + for relocation_section in elf_sections: + if relocation_section.kind != SHT_REL: + continue + if relocation_section.entry_size != ELF_RELOCATION_SIZE: + raise CoffError( + f"unsupported relocation size in {relocation_section.name!r}: " + f"{relocation_section.entry_size}" + ) + if relocation_section.info >= len(elf_sections): + raise CoffError( + f"{relocation_section.name!r} has an invalid target section" + ) + target = elf_sections[relocation_section.info] + coff_section = coff_by_name.get(target.name) + if coff_section is None: + # The conversion intentionally removes unwind tables and may omit + # other sections that have no representation in the final object. + continue + if relocation_section.size % ELF_RELOCATION_SIZE: + raise CoffError( + f"{relocation_section.name!r} has a partial relocation record" + ) + elf_count = relocation_section.size // ELF_RELOCATION_SIZE + if elf_count != coff_section.relocation_count: + raise CoffError( + f"relocation count differs for {target.name!r}: " + f"ELF={elf_count}, COFF={coff_section.relocation_count}" + ) + matched_coff_sections.add(coff_section.index) + + for index in range(elf_count): + elf_record = ( + relocation_section.offset + index * ELF_RELOCATION_SIZE + ) + relocation_address, relocation_info = struct.unpack_from( + "> 8 + input_type = relocation_info & 0xFF + if elf_symbol_index >= len(elf_symbols): + raise CoffError( + f"{relocation_section.name!r} references invalid ELF " + f"symbol {elf_symbol_index}" + ) + try: + ELF_TO_WINCE_RELOCATION[input_type] + except KeyError as error: + raise CoffError( + f"unsupported ARM ELF relocation {input_type} in " + f"{target.name!r}, relocation {index}" + ) from error + + coff_record = ( + coff_section.relocation_offset + index * COFF_RELOCATION_SIZE + ) + coff_address = read_u32(coff_data, coff_record) + copied_type = read_u16(coff_data, coff_record + 8) + if coff_address != relocation_address or copied_type != input_type: + raise CoffError( + f"ELF/COFF relocation order differs in {target.name!r} " + f"at index {index}: ELF=(0x{relocation_address:x}, " + f"{input_type}), COFF=(0x{coff_address:x}, {copied_type})" + ) + if input_type in (28, 29): + if relocation_address > coff_section.data_size - 4: + raise CoffError( + f"ARM branch relocation at 0x{relocation_address:x} " + f"lies outside {target.name!r}" + ) + instruction_offset = ( + coff_section.data_offset + relocation_address + ) + instruction = read_u32(coff_data, instruction_offset) + if instruction & 0x0E000000 != 0x0A000000: + raise CoffError( + f"ARM branch relocation at 0x{relocation_address:x} " + f"in {target.name!r} targets non-branch instruction " + f"0x{instruction:08x}" + ) + # ELF REL stores A=-8 in imm24 so S+A-P compensates for the + # ARM PC value (P+8). WinCE ARM_26 performs that compensation + # itself and follows CeGCC's convention of imm24=0. + struct.pack_into( + " int: + """Reject an ARM_26 relocation whose instruction still carries an addend.""" + verified = 0 + for section in parse_coff_sections(data): + for index in range(section.relocation_count): + record = ( + section.relocation_offset + index * COFF_RELOCATION_SIZE + ) + if read_u16(data, record + 8) != IMAGE_REL_ARM_BRANCH24: + continue + address = read_u32(data, record) + if address > section.data_size - 4: + raise CoffError( + f"ARM_26 relocation at 0x{address:x} lies outside " + f"{section.name!r}" + ) + instruction = read_u32(data, section.data_offset + address) + if instruction & 0x0E000000 != 0x0A000000: + raise CoffError( + f"ARM_26 relocation at 0x{address:x} in " + f"{section.name!r} references non-branch instruction " + f"0x{instruction:08x}" + ) + if instruction & 0x00FFFFFF: + raise CoffError( + f"ARM_26 relocation at 0x{address:x} in " + f"{section.name!r} retains non-zero instruction addend " + f"0x{instruction & 0x00FFFFFF:06x}" + ) + verified += 1 + return verified + + +def restore_ui_exports(data: bytearray) -> tuple[bytearray, list[str]]: + section_count = read_u16(data, 2) + symbol_table = read_u32(data, 8) + symbol_count = read_u32(data, 12) + optional_header_size = read_u16(data, 16) + section_table = COFF_HEADER_SIZE + optional_header_size + string_table = symbol_table + symbol_count * COFF_SYMBOL_SIZE + + checked_range( + data, + symbol_table, + symbol_count * COFF_SYMBOL_SIZE, + "COFF symbol table", + ) + checked_range(data, string_table, 4, "COFF string table") + string_table_size = read_u32(data, string_table) + if string_table_size < 4: + raise CoffError(f"invalid COFF string-table size {string_table_size}") + checked_range(data, string_table, string_table_size, "COFF string table") + string_table_end = string_table + string_table_size + + existing_symbols: set[str] = set() + symbol_index = 0 + while symbol_index < symbol_count: + record = symbol_table + symbol_index * COFF_SYMBOL_SIZE + if data[record + 16] == IMAGE_SYM_CLASS_EXTERNAL: + existing_symbols.add( + symbol_name(data, record, string_table, string_table_end) + ) + auxiliary_count = data[record + 17] + symbol_index += 1 + auxiliary_count + if symbol_index != symbol_count: + raise CoffError("COFF auxiliary symbol records exceed the symbol table") + + ui_sections: dict[str, int] = {} + for section_index in range(section_count): + header = section_table + section_index * COFF_SECTION_SIZE + name = section_name(data, header, string_table, string_table_end) + if name.startswith(".text.ui_"): + ui_sections[name.removeprefix(".text.")] = section_index + 1 + + missing = sorted(set(ui_sections) - existing_symbols) + if not missing: + return data, [] + + strings = bytearray(data[string_table:string_table_end]) + string_offsets: dict[str, int] = {} + cursor = 4 + while cursor < len(strings): + end = strings.find(b"\0", cursor) + if end < 0: + end = len(strings) + string_offsets[bytes(strings[cursor:end]).decode("ascii")] = cursor + cursor = end + 1 + if strings[-1] != 0: + strings.append(0) + + added_symbols = bytearray() + for name in missing: + encoded = name.encode("ascii") + if len(encoded) <= 8: + name_field = encoded.ljust(8, b"\0") + else: + offset = string_offsets.get(name) + if offset is None: + offset = len(strings) + strings.extend(encoded) + strings.append(0) + string_offsets[name] = offset + name_field = b"\0\0\0\0" + struct.pack(" int: + parser = argparse.ArgumentParser() + parser.add_argument("elf_input", type=Path) + parser.add_argument("coff_input", type=Path) + parser.add_argument("output", type=Path) + args = parser.parse_args() + + elf_data = args.elf_input.read_bytes() + data = bytearray(args.coff_input.read_bytes()) + data, patched, mapped_symbols, normalized_branches = remap_relocations( + elf_data, data + ) + data, restored = restore_ui_exports(data) + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_bytes(data) + + summary = ", ".join( + f"{source}->{target}: {count}" + for (source, target), count in sorted(patched.items()) + ) + print(f"patched {sum(patched.values())} ARM relocations ({summary})") + print( + f"normalized and verified {normalized_branches} " + "WinCE ARM_26 branch addends" + ) + print(f"mapped {mapped_symbols} ELF relocation symbols") + print(f"restored {len(restored)} PocketJS C ABI exports") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/hosts/wm6/quickjs/README.md b/hosts/wm6/quickjs/README.md new file mode 100644 index 000000000..45174701f --- /dev/null +++ b/hosts/wm6/quickjs/README.md @@ -0,0 +1,102 @@ +# QuickJS for Windows Mobile 6 + +This directory is the first executable QuickJS toolchain milestone for the +WM6 port. It builds the repository's pinned `pocket-stack/quickjs-rs` revision +as an ARM Windows CE application with the native CeGCC toolset. Code generation +uses the SDK's ARMV4I-compatible instruction baseline (`-march=armv4t`), so +the same binary runs in the WM6 emulator and on the ARMv5TE PXA310. + +Visual C++ 2005 remains the windowing, deployment, and debugger tool for the +PocketJS host. QuickJS itself uses CeGCC because current QuickJS is +GNU C99 and cannot be compiled by VC8. A narrow C DLL API is the integration +boundary; QuickJS values and allocations stay on the CeGCC side. + +## Pinned source + +- repository: `https://github.com/pocket-stack/quickjs-rs` +- revision: `0fc946fb670c0c29bc0135f510bcb0f595415a61` +- QuickJS version: `2026-06-04` + +These values deliberately match `tools/cli/symbian-toolchain.json`. + +## Build the probe + +Install or extract the native-API `mingw32ce` CeGCC toolchain, then run: + +```sh +WM6_CEGCC_ROOT=/opt/mingw32ce-0.59.1 \ + hosts/wm6/quickjs/build-probe.sh +``` + +Old CeGCC Linux binaries may require compatible 32-bit MPFR, GMP, and zlib +shared libraries. If they are not installed system-wide, point the loader at +their directory: + +```sh +WM6_CEGCC_ROOT=/opt/mingw32ce-0.59.1 \ +WM6_CEGCC_LIBDIR=/opt/mingw32ce-compat/lib \ + hosts/wm6/quickjs/build-probe.sh +``` + +For a repeat build without another network fetch, set `WM6_QUICKJS_SOURCE` to +a local checkout at the pinned revision. + +The default output is +`hosts/wm6/vs2005/prebuilt/PocketJS.WM6.QuickJS.Probe.exe`. The executable +performs 100 create/evaluate/drain/destroy cycles with an 8 MiB-limited +QuickJS runtime and 256 KiB stack. Each cycle calls a native `print` function +from a Promise job. Success shows `QuickJS 6,10,16,26` under the title +`QuickJS: 100 cycles passed` in a native WM6 message box. + +## Hero demo host + +`build-runtime.sh` builds `PocketJS.WM6.QuickJS.v3.dll` with a versioned C ABI, +statically linked libgcc, and the native Rust core produced by +`engine/wm6/build-core.sh`. The DLL installs the same `ui` HostOps boundary as +the Symbian host: JavaScript node/style/texture/animation calls go directly to +the real retained Rust `Ui`. It also copies the PAK into QuickJS before mount, +advances `globalThis.frame` and `ui_tick` at the host cadence, and exposes the +core's incremental ARGB32 framebuffer. + +`build-demo.sh` packages the unmodified real `dist/hero-main.js` output; it no +longer prepends a JavaScript tree or hand-authored draw-list adapter. +VS2005 builds `PocketJS.WM6.QuickJS.exe`, deploys the DLL, bundle, and PAK, +and mounts the Solid application. The native +screen size after WM6 rotation becomes `ui.__viewport`. Each Rust ARGB32 frame +is converted to the application-owned RGB565 buffer and presented through a +locked DirectDraw primary surface. Arrow keys and Enter/Space are mapped to the +PocketJS directional and Circle button bits. Stylus contacts use the wide +PocketJS touch encoding so the full 640×480 VGA coordinates are preserved. +The ABI suffix is part of the deployed filename so Windows CE cannot satisfy a +new host from an older process's shared/cached runtime module. + +The compatibility patch: + +- disables QuickJS atomics because WM6 has no pthread/C11 atomic API; +- uses GCC built-ins for stack allocation and `signbit`; +- avoids `_msize`, which is absent from the Windows CE CRT; +- temporarily reports UTC for `Date#getTimezoneOffset`; +- supplies the C99 `fmax` and `fmin` functions missing from the CE CRT. + +The UTC fallback is intentionally conservative and must be replaced with a +WinCE `SYSTEMTIME`/timezone implementation before shipping a general runtime. + +## Native integration smoke test + +`test-runtime-native.sh` compiles the exact WM6 QuickJS bridge against the +same Rust core on the development host, evaluates the checked-in Hero bundle, +installs its PAK, forwards one wide touch frame, and verifies a non-empty +ARGB32 framebuffer. It defaults to 640×480; optional third and fourth +arguments select the exact viewport so the 320×240 emulator path can be +checked as well. This does not replace ARMV4I emulator acceptance, but it +catches JS/HostOps/core integration regressions before deployment: + +```sh +WM6_QUICKJS_SOURCE=/path/to/pinned/quickjs-rs \ + hosts/wm6/quickjs/test-runtime-native.sh + +WM6_QUICKJS_SOURCE=/path/to/pinned/quickjs-rs \ + hosts/wm6/quickjs/test-runtime-native.sh \ + hosts/wm6/vs2005/prebuilt/PocketJS.WM6.Demo.js \ + hosts/wm6/vs2005/prebuilt/PocketJS.WM6.Demo.pak 320 240 +``` diff --git a/hosts/wm6/quickjs/build-cards.sh b/hosts/wm6/quickjs/build-cards.sh new file mode 100644 index 000000000..e8f12d82a --- /dev/null +++ b/hosts/wm6/quickjs/build-cards.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "${script_dir}/../../.." && pwd)" +bundle="${repo_root}/dist/cards-main.js" +pak="${repo_root}/dist/cards-main.pak" +output="${1:-${script_dir}/../vs2005/prebuilt/PocketJS.WM6.Cards.js}" +pak_output="${output%.*}.pak" + +if [[ ! -f "$bundle" || ! -f "$pak" ]]; then + echo "Missing Cards bundle or pak; run: bun tools/build.ts cards-main" >&2 + exit 2 +fi +mkdir -p "$(dirname "$output")" +{ + printf '%s\n' '/* PocketJS WM6 bootstrap + real apps/cards bundle. */' + sed -n 'p' "${script_dir}/cards-host.js" + sed -n 'p' "$bundle" +} > "$output" +cp "$pak" "$pak_output" +echo "Built ${output} and ${pak_output}" diff --git a/hosts/wm6/quickjs/build-demo.sh b/hosts/wm6/quickjs/build-demo.sh new file mode 100644 index 000000000..f309c674e --- /dev/null +++ b/hosts/wm6/quickjs/build-demo.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "${script_dir}/../../.." && pwd)" +bundle="${repo_root}/dist/hero-main.js" +pak="${repo_root}/dist/hero-main.pak" +output="${1:-${script_dir}/../vs2005/prebuilt/PocketJS.WM6.Demo.js}" +pak_output="${output%.*}.pak" + +if [[ ! -f "$bundle" || ! -f "$pak" ]]; then + echo "Missing Hero bundle or pak; run: bun tools/build.ts hero-main" >&2 + exit 2 +fi +mkdir -p "$(dirname "$output")" +{ + printf '%s\n' '/* PocketJS WM6 native Rust core + real apps/hero bundle. */' + sed -n 'p' "$bundle" +} > "$output" +cp "$pak" "$pak_output" +echo "Built ${output} and ${pak_output}" diff --git a/hosts/wm6/quickjs/build-probe.sh b/hosts/wm6/quickjs/build-probe.sh new file mode 100644 index 000000000..68347945a --- /dev/null +++ b/hosts/wm6/quickjs/build-probe.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +set -euo pipefail + +quickjs_repo="https://github.com/pocket-stack/quickjs-rs" +quickjs_rev="0fc946fb670c0c29bc0135f510bcb0f595415a61" +quickjs_version="2026-06-04" + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +output="${1:-${script_dir}/../vs2005/prebuilt/PocketJS.WM6.QuickJS.Probe.exe}" +work_dir="$(mktemp -d "${TMPDIR:-/tmp}/pocketjs-wm6-quickjs.XXXXXX")" +trap 'rm -rf "$work_dir"' EXIT + +if [[ -z "${WM6_CEGCC_ROOT:-}" ]]; then + echo "WM6_CEGCC_ROOT must point at an extracted mingw32ce toolchain." >&2 + exit 2 +fi + +tool_bin="${WM6_CEGCC_ROOT}/mingw32ce/bin" +cc="${tool_bin}/arm-mingw32ce-gcc" +if [[ ! -x "$cc" ]]; then + echo "arm-mingw32ce-gcc was not found under ${tool_bin}." >&2 + exit 2 +fi + +if [[ -n "${WM6_CEGCC_LIBDIR:-}" ]]; then + export LD_LIBRARY_PATH="${WM6_CEGCC_LIBDIR}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" +fi + +if [[ -n "${WM6_QUICKJS_SOURCE:-}" ]]; then + if [[ "$(git -C "$WM6_QUICKJS_SOURCE" rev-parse HEAD)" != "$quickjs_rev" ]]; then + echo "WM6_QUICKJS_SOURCE is not at the pinned revision ${quickjs_rev}." >&2 + exit 2 + fi + cp -R "$WM6_QUICKJS_SOURCE" "${work_dir}/quickjs-rs" +else + git init -q "${work_dir}/quickjs-rs" + git -C "${work_dir}/quickjs-rs" remote add origin "$quickjs_repo" + git -C "${work_dir}/quickjs-rs" fetch -q --depth 1 origin "$quickjs_rev" + git -C "${work_dir}/quickjs-rs" checkout -q FETCH_HEAD +fi + +quickjs_dir="${work_dir}/quickjs-rs/libquickjs-sys/embed/quickjs" +patch -d "$quickjs_dir" -p1 < "${script_dir}/patches/quickjs-wm6.patch" +mkdir -p "${work_dir}/obj" "$(dirname "$output")" +cp "${script_dir}/src/wm6_math.c" "${work_dir}/wm6_math.c" +cp "${script_dir}/src/probe.c" "${work_dir}/probe.c" + +common_flags=( + -std=gnu99 + -march=armv4t + -msoft-float + -Os + -funsigned-char + -D_WIN32_WCE=0x0502 + -DWINCE + -D_WIN32 + "-DCONFIG_VERSION=\"${quickjs_version}\"" + "-I${quickjs_dir}" +) + +for source in cutils dtoa libregexp libunicode quickjs; do + "$cc" "${common_flags[@]}" -c "${quickjs_dir}/${source}.c" \ + -o "${work_dir}/obj/${source}.o" +done +"$cc" "${common_flags[@]}" -c "${work_dir}/wm6_math.c" \ + -o "${work_dir}/obj/wm6_math.o" +"$cc" "${common_flags[@]}" -c "${work_dir}/probe.c" \ + -o "${work_dir}/obj/probe.o" + +"$cc" -march=armv4t -msoft-float -mwindows \ + -o "${work_dir}/PocketJS.WM6.QuickJS.Probe.exe" \ + "${work_dir}/obj/probe.o" \ + "${work_dir}/obj/wm6_math.o" \ + "${work_dir}/obj/quickjs.o" \ + "${work_dir}/obj/cutils.o" \ + "${work_dir}/obj/dtoa.o" \ + "${work_dir}/obj/libregexp.o" \ + "${work_dir}/obj/libunicode.o" \ + -lm +cp "${work_dir}/PocketJS.WM6.QuickJS.Probe.exe" "$output" + +echo "Built ${output}" diff --git a/hosts/wm6/quickjs/build-runtime.sh b/hosts/wm6/quickjs/build-runtime.sh new file mode 100644 index 000000000..6c751c03e --- /dev/null +++ b/hosts/wm6/quickjs/build-runtime.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +set -euo pipefail + +quickjs_repo="https://github.com/pocket-stack/quickjs-rs" +quickjs_rev="0fc946fb670c0c29bc0135f510bcb0f595415a61" +quickjs_version="2026-06-04" + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +output="${1:-${script_dir}/../vs2005/prebuilt/PocketJS.WM6.QuickJS.v3.dll}" +core_object="${WM6_CORE_OBJECT:-${script_dir}/../vs2005/prebuilt/PocketJS.WM6.Core.obj}" +work_dir="$(mktemp -d "${TMPDIR:-/tmp}/pocketjs-wm6-quickjs-dll.XXXXXX")" +trap 'rm -rf "$work_dir"' EXIT + +if [[ -z "${WM6_CEGCC_ROOT:-}" ]]; then + echo "WM6_CEGCC_ROOT must point at an extracted mingw32ce toolchain." >&2 + exit 2 +fi +tool_bin="${WM6_CEGCC_ROOT}/mingw32ce/bin" +cc="${tool_bin}/arm-mingw32ce-gcc" +if [[ ! -x "$cc" ]]; then + echo "arm-mingw32ce-gcc was not found under ${tool_bin}." >&2 + exit 2 +fi +if [[ ! -f "$core_object" ]]; then + echo "PocketJS WM6 core object not found: ${core_object}" >&2 + echo "Build it first with engine/wm6/build-core.sh." >&2 + exit 2 +fi +if [[ -n "${WM6_CEGCC_LIBDIR:-}" ]]; then + export LD_LIBRARY_PATH="${WM6_CEGCC_LIBDIR}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" +fi + +if [[ -n "${WM6_QUICKJS_SOURCE:-}" ]]; then + if [[ "$(git -C "$WM6_QUICKJS_SOURCE" rev-parse HEAD)" != "$quickjs_rev" ]]; then + echo "WM6_QUICKJS_SOURCE is not at the pinned revision ${quickjs_rev}." >&2 + exit 2 + fi + cp -R "$WM6_QUICKJS_SOURCE" "${work_dir}/quickjs-rs" +else + git init -q "${work_dir}/quickjs-rs" + git -C "${work_dir}/quickjs-rs" remote add origin "$quickjs_repo" + git -C "${work_dir}/quickjs-rs" fetch -q --depth 1 origin "$quickjs_rev" + git -C "${work_dir}/quickjs-rs" checkout -q FETCH_HEAD +fi + +quickjs_dir="${work_dir}/quickjs-rs/libquickjs-sys/embed/quickjs" +patch -d "$quickjs_dir" -p1 < "${script_dir}/patches/quickjs-wm6.patch" +mkdir -p "${work_dir}/obj" "$(dirname "$output")" +cp "${script_dir}/src/wm6_math.c" "${work_dir}/wm6_math.c" +cp "${script_dir}/src/runtime_dll.c" "${work_dir}/runtime_dll.c" +cp "${script_dir}/src/pocketjs_wm6_core.h" \ + "${work_dir}/pocketjs_wm6_core.h" +cp "${script_dir}/../vs2005/runtime/wm6_quickjs_abi.h" \ + "${work_dir}/wm6_quickjs_abi.h" + +common_flags=( + -std=gnu99 -march=armv4t -msoft-float -Os -funsigned-char + -D_WIN32_WCE=0x0502 -DWINCE -D_WIN32 + "-DCONFIG_VERSION=\"${quickjs_version}\"" + "-I${quickjs_dir}" "-I${work_dir}" +) +for source in cutils dtoa libregexp libunicode quickjs; do + "$cc" "${common_flags[@]}" -c "${quickjs_dir}/${source}.c" \ + -o "${work_dir}/obj/${source}.o" +done +"$cc" "${common_flags[@]}" -c "${work_dir}/wm6_math.c" \ + -o "${work_dir}/obj/wm6_math.o" +"$cc" "${common_flags[@]}" -c "${work_dir}/runtime_dll.c" \ + -o "${work_dir}/obj/runtime_dll.o" + +"$cc" -shared -static-libgcc -march=armv4t -msoft-float \ + -o "${work_dir}/PocketJS.WM6.QuickJS.v3.dll" \ + "${work_dir}/obj/runtime_dll.o" "${work_dir}/obj/wm6_math.o" \ + "${work_dir}/obj/quickjs.o" "${work_dir}/obj/cutils.o" \ + "${work_dir}/obj/dtoa.o" "${work_dir}/obj/libregexp.o" \ + "${work_dir}/obj/libunicode.o" "$core_object" -lm +cp "${work_dir}/PocketJS.WM6.QuickJS.v3.dll" "$output" +echo "Built ${output}" diff --git a/hosts/wm6/quickjs/cards-host.js b/hosts/wm6/quickjs/cards-host.js new file mode 100644 index 000000000..441b1f0c0 --- /dev/null +++ b/hosts/wm6/quickjs/cards-host.js @@ -0,0 +1,136 @@ +(() => { + let nextNode = 2; + let nextAnim = 1; + let focused = 0; + const nodes = new Map(); + nodes.set(1, { id: 1, type: 0, style: -1, text: "", parent: 0, children: [] }); + + const node = (id) => nodes.get(id); + const detach = (id) => { + for (const parent of nodes.values()) { + const at = parent.children.indexOf(id); + if (at >= 0) parent.children.splice(at, 1); + } + const child = node(id); + if (child) child.parent = 0; + }; + + globalThis.ui = { + __host: "wm6", + __hostAbi: 1, + __textures: {}, + __viewport: { w: 480, h: 272 }, + createNode(type) { + const id = nextNode++; + nodes.set(id, { id, type, style: -1, text: "", parent: 0, children: [] }); + return id; + }, + destroyNode(id) { + detach(id); + nodes.delete(id); + }, + insertBefore(parentId, childId, anchor) { + const parent = node(parentId); + if (!parent) return; + detach(childId); + const at = anchor ? parent.children.indexOf(anchor) : -1; + if (at >= 0) parent.children.splice(at, 0, childId); + else parent.children.push(childId); + const child = node(childId); + if (child) child.parent = parentId; + }, + removeChild(_parent, child) { detach(child); }, + setStyle(id, style) { const n = node(id); if (n) n.style = style; }, + setProp() {}, + setText(id, text) { const n = node(id); if (n) n.text = String(text); }, + replaceText(id, text) { this.setText(id, text); }, + uploadTexture() { return -1; }, + setImage() {}, + setSprite() {}, + animate() { return nextAnim++; }, + cancelAnim() {}, + setFocus(id) { focused = id; }, + setActive() {}, + measureText(text, fontSlot) { + const width = fontSlot === 12 ? 13 : fontSlot === 8 ? 8 : 7; + return String(text).length * width; + } + }; + + globalThis.__wm6Snapshot = () => { + const lines = ["PocketJS Cards (real bundle)", "viewport 480x272", ""]; + const visit = (id, depth) => { + const n = node(id); + if (!n) return; + if (n.text) lines.push(" ".repeat(depth) + n.text); + for (const child of n.children) visit(child, depth + 1); + }; + visit(1, 0); + return lines.join("\n"); + }; + + globalThis.__wm6DrawList = () => { + const out = ["B|248|250|252"]; + const safe = (text) => String(text).replace(/[|\r\n]/g, " "); + const text = (x, y, slot, r, g, b, value) => + out.push(`T|${x}|${y}|${slot}|${r}|${g}|${b}|${safe(value)}`); + const rect = (x, y, w, h, r, g, b) => + out.push(`R|${x}|${y}|${w}|${h}|${r}|${g}|${b}`); + let cardIndex = 0; + const effectiveStyle = (n) => { + let current = n; + while (current) { + if (current.style >= 0) return current.style; + current = node(current.parent); + } + return -1; + }; + + for (const n of nodes.values()) { + if (!n.text) continue; + const style = effectiveStyle(n); + if (style === 18) text(16, 18, 0, 37, 99, 235, n.text); + else if (style === 19) text(16, 35, 12, 15, 23, 42, n.text); + else if (style === 20 && n.text === "3 MODULES") + text(398, 43, 0, 100, 116, 139, n.text); + else if (style === 20) + text(16, 250, 0, 100, 116, 139, n.text); + } + for (const n of nodes.values()) { + if (n.style !== 0 && n.style !== 3 && n.style !== 6) continue; + const x = 16 + cardIndex * 148; + const isFocused = n.id === focused; + const accent = n.style === 0 ? [59, 130, 246] + : n.style === 3 ? [16, 185, 129] : [245, 158, 11]; + rect(x, 76, 136, 82, isFocused ? 239 : 255, + isFocused ? 246 : 255, isFocused ? 255 : 255); + rect(x, 76, 136, 4, accent[0], accent[1], accent[2]); + const labels = []; + const collect = (id) => { + const child = node(id); + if (!child) return; + if (child.text) labels.push(child.text); + for (const nested of child.children) collect(nested); + }; + collect(n.id); + if (labels[0]) text(x + 12, 91, 8, 15, 23, 42, labels[0]); + if (labels[1]) text(x + 12, 115, 0, 71, 85, 105, labels[1]); + cardIndex++; + } + for (const n of nodes.values()) { + if (n.style !== 9) continue; + rect(16, 174, 448, 54, 255, 255, 255); + const labels = []; + const collect = (id) => { + const child = node(id); + if (!child) return; + if (child.text) labels.push(child.text); + for (const nested of child.children) collect(nested); + }; + collect(n.id); + if (labels[0]) text(34, 184, 8, 15, 23, 42, labels[0]); + if (labels[1]) text(34, 204, 0, 71, 85, 105, labels[1]); + } + return out.join("\n"); + }; +})(); diff --git a/hosts/wm6/quickjs/patches/quickjs-wm6.patch b/hosts/wm6/quickjs/patches/quickjs-wm6.patch new file mode 100644 index 000000000..9ec8a5f4b --- /dev/null +++ b/hosts/wm6/quickjs/patches/quickjs-wm6.patch @@ -0,0 +1,48 @@ +diff --git a/quickjs.c b/quickjs.c +index 39e334c..c45f717 100644 +--- a/quickjs.c ++++ b/quickjs.c +@@ -34,6 +34,12 @@ + #include + #include + #include ++#if defined(_WIN32_WCE) ++#define alloca __builtin_alloca ++#ifndef signbit ++#define signbit __builtin_signbit ++#endif ++#endif + #ifdef __PSP__ + #ifndef FE_TONEAREST + #define FE_TONEAREST 0 +@@ -100,6 +106,9 @@ _Static_assert(_Alignof(JSValue) == 8, "Vita JSValue must be 8-byte aligned"); + #if defined(__PSP__) || defined(__vita__) + #undef CONFIG_ATOMICS + #endif ++#if defined(_WIN32_WCE) ++#undef CONFIG_ATOMICS ++#endif + + #if !defined(__EMSCRIPTEN__) + /* enable stack limitation */ +@@ -2167,6 +2176,8 @@ static size_t js_def_malloc_usable_size(const void *ptr) + { + #if defined(__APPLE__) + return malloc_size(ptr); ++#elif defined(_WIN32_WCE) ++ return 0; + #elif defined(_WIN32) + return _msize((void *)ptr); + #elif defined(__EMSCRIPTEN__) +@@ -47284,7 +47295,10 @@ static int getTimezoneOffset(int64_t time) + } + } + ti = time; +-#if defined(_WIN32) ++#if defined(_WIN32_WCE) ++ (void)ti; ++ res = 0; ++#elif defined(_WIN32) + { + struct tm *tm; + time_t gm_ti, loc_ti; diff --git a/hosts/wm6/quickjs/src/pocketjs_wm6_core.h b/hosts/wm6/quickjs/src/pocketjs_wm6_core.h new file mode 100644 index 000000000..165ed4994 --- /dev/null +++ b/hosts/wm6/quickjs/src/pocketjs_wm6_core.h @@ -0,0 +1,73 @@ +#ifndef POCKETJS_WM6_CORE_H +#define POCKETJS_WM6_CORE_H + +#include +#include + +void ui_init(uint32_t raster_density); +void ui_shutdown(void); +void ui_set_viewport(float width, float height); + +int32_t ui_create_node(uint32_t node_type); +void ui_destroy_node(int32_t id); +void ui_insert_before(int32_t parent, int32_t child, int32_t anchor); +void ui_remove_child(int32_t parent, int32_t child); +void ui_set_style(int32_t id, int32_t style_id); +void ui_set_prop(int32_t id, uint32_t prop, double value); +void ui_set_prop_batch(const uint8_t *data, size_t len); +void ui_set_text(int32_t id, const uint8_t *text, size_t len); +void ui_replace_text(int32_t id, const uint8_t *text, size_t len); + +int32_t ui_upload_texture( + const uint8_t *data, + size_t len, + uint32_t width, + uint32_t height, + uint32_t psm); +int32_t ui_upload_img_entry(const uint8_t *data, size_t len); +void ui_free_texture(int32_t handle); +void ui_set_image(int32_t id, int32_t texture); +void ui_set_sprite( + int32_t id, + int32_t atlas, + uint32_t frames, + uint32_t columns, + uint32_t step); + +int32_t ui_animate( + int32_t id, + uint32_t prop, + double to, + uint32_t duration_ms, + uint32_t easing, + uint32_t delay_ms); +void ui_cancel_anim(int32_t animation_id); +void ui_set_focus(int32_t id); +void ui_set_active(int32_t id, int32_t active); +int32_t ui_hit_test(float x, float y); +void ui_set_cursor( + int32_t texture, + float hot_x, + float hot_y, + float width, + float height); +void ui_set_cursor_pos(float x, float y); + +int32_t ui_load_styles(const uint8_t *data, size_t len); +int32_t ui_load_font_atlas(const uint8_t *data, size_t len); +float ui_measure_text(const uint8_t *text, size_t len, uint32_t font_slot); + +void ui_debug_inspect(int32_t id); +int32_t ui_debug_rect_xy(void); +int32_t ui_debug_rect_wh(void); +void ui_debug_pause(int32_t on); +void ui_debug_step(void); + +void ui_tick(void); +const uint8_t *ui_render_incremental(void); +uint32_t ui_framebuffer_width(void); +uint32_t ui_framebuffer_height(void); +uint32_t ui_framebuffer_stride(void); +size_t ui_framebuffer_len(void); + +#endif diff --git a/hosts/wm6/quickjs/src/probe.c b/hosts/wm6/quickjs/src/probe.c new file mode 100644 index 000000000..9971eb2b1 --- /dev/null +++ b/hosts/wm6/quickjs/src/probe.c @@ -0,0 +1,152 @@ +#include +#include + +#include "quickjs.h" + +typedef struct ProbeState { + char text[128]; + int print_count; +} ProbeState; + +static void copy_ascii(char *out, unsigned int capacity, const char *text) +{ + unsigned int index; + + if (capacity == 0) + return; + index = 0; + while (text[index] != '\0' && index + 1 < capacity) { + out[index] = text[index]; + index++; + } + out[index] = '\0'; +} + +static void ascii_to_wide(WCHAR *out, unsigned int capacity, const char *text) +{ + unsigned int index; + + if (capacity == 0) + return; + index = 0; + while (text[index] != '\0' && index + 1 < capacity) { + unsigned char ch = (unsigned char)text[index]; + out[index] = ch < 128 ? (WCHAR)ch : L'?'; + index++; + } + out[index] = L'\0'; +} + +static JSValue probe_print(JSContext *context, JSValueConst this_value, + int argument_count, JSValueConst *arguments) +{ + ProbeState *state; + const char *text; + + (void)this_value; + state = (ProbeState *)JS_GetContextOpaque(context); + if (!state || argument_count < 1) + return JS_UNDEFINED; + text = JS_ToCString(context, arguments[0]); + if (!text) + return JS_EXCEPTION; + copy_ascii(state->text, sizeof(state->text), text); + state->print_count++; + JS_FreeCString(context, text); + return JS_UNDEFINED; +} + +static int run_cycle(ProbeState *state) +{ + static const char source[] = + "(() => {" + " const values = [3, 5, 8, 13];" + " const result = 'QuickJS ' + values.map(x => x * 2).join(',');" + " return Promise.resolve().then(() => print(result));" + "})()"; + JSRuntime *runtime; + JSContext *context; + JSContext *job_context; + JSValue global; + JSValue result; + const char *text; + int exit_code; + + runtime = JS_NewRuntime(); + if (!runtime) + return 1; + JS_SetMemoryLimit(runtime, 8u * 1024u * 1024u); + JS_SetMaxStackSize(runtime, 256u * 1024u); + context = JS_NewContext(runtime); + if (!context) { + JS_FreeRuntime(runtime); + return 2; + } + JS_SetContextOpaque(context, state); + global = JS_GetGlobalObject(context); + if (JS_SetPropertyStr(context, global, "print", + JS_NewCFunction(context, probe_print, "print", 1)) < 0) { + JS_FreeValue(context, global); + JS_FreeContext(context); + JS_FreeRuntime(runtime); + return 3; + } + JS_FreeValue(context, global); + + result = JS_Eval(context, source, sizeof(source) - 1, + "wm6-probe.js", JS_EVAL_TYPE_GLOBAL); + exit_code = 0; + if (JS_IsException(result)) { + JSValue exception = JS_GetException(context); + text = JS_ToCString(context, exception); + copy_ascii(state->text, sizeof(state->text), + text ? text : "JavaScript exception"); + if (text) + JS_FreeCString(context, text); + JS_FreeValue(context, exception); + exit_code = 4; + } + JS_FreeValue(context, result); + while (exit_code == 0 && JS_IsJobPending(runtime)) { + job_context = NULL; + if (JS_ExecutePendingJob(runtime, &job_context) < 0) + exit_code = 5; + } + if (exit_code == 0 && state->print_count != 1) + exit_code = 6; + JS_FreeContext(context); + JS_FreeRuntime(runtime); + return exit_code; +} + +int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPWSTR command, int show) +{ + ProbeState state; + WCHAR message[256]; + int cycle; + int exit_code; + + (void)instance; + (void)previous; + (void)command; + (void)show; + + exit_code = 0; + for (cycle = 0; cycle < 100; cycle++) { + state.text[0] = '\0'; + state.print_count = 0; + exit_code = run_cycle(&state); + if (exit_code != 0) + break; + } + if (exit_code == 0) { + /* Expected result: QuickJS 6,10,16,26 */ + ascii_to_wide(message, 256, state.text); + MessageBox(NULL, message, L"QuickJS: 100 cycles passed", MB_OK); + } else { + ascii_to_wide(message, 256, + state.text[0] ? state.text : "QuickJS probe failed"); + MessageBox(NULL, message, L"PocketJS QuickJS failure", MB_OK); + } + return exit_code; +} diff --git a/hosts/wm6/quickjs/src/runtime_dll.c b/hosts/wm6/quickjs/src/runtime_dll.c new file mode 100644 index 000000000..c46188d72 --- /dev/null +++ b/hosts/wm6/quickjs/src/runtime_dll.c @@ -0,0 +1,792 @@ +#include +#include +#include + +#include "pocketjs_wm6_core.h" +#include "quickjs.h" +#include "wm6_quickjs_abi.h" + +typedef struct Wm6QuickJS { + JSRuntime *runtime; + JSContext *context; + char printed[256]; + int first_frame_traced; +} Wm6QuickJS; + +typedef enum Wm6HostOperation { + WM6_HOST_CREATE_NODE, + WM6_HOST_DESTROY_NODE, + WM6_HOST_INSERT_BEFORE, + WM6_HOST_REMOVE_CHILD, + WM6_HOST_SET_STYLE, + WM6_HOST_SET_PROP, + WM6_HOST_SET_PROP_BATCH, + WM6_HOST_SET_TEXT, + WM6_HOST_REPLACE_TEXT, + WM6_HOST_UPLOAD_TEXTURE, + WM6_HOST_UPLOAD_IMG_ENTRY, + WM6_HOST_FREE_TEXTURE, + WM6_HOST_SET_IMAGE, + WM6_HOST_SET_SPRITE, + WM6_HOST_ANIMATE, + WM6_HOST_CANCEL_ANIM, + WM6_HOST_SET_FOCUS, + WM6_HOST_SET_ACTIVE, + WM6_HOST_HIT_TEST, + WM6_HOST_SET_CURSOR, + WM6_HOST_SET_CURSOR_POS, + WM6_HOST_LOAD_STYLES, + WM6_HOST_LOAD_FONT_ATLAS, + WM6_HOST_MEASURE_TEXT, + WM6_HOST_DEBUG_INSPECT, + WM6_HOST_DEBUG_RECT_XY, + WM6_HOST_DEBUG_RECT_WH, + WM6_HOST_DEBUG_PAUSE, + WM6_HOST_DEBUG_STEP +} Wm6HostOperation; + +static void copy_text(char *output, unsigned int capacity, const char *text) +{ + unsigned int index; + + if (!output || capacity == 0) + return; + index = 0; + while (text && text[index] != '\0' && index + 1 < capacity) { + output[index] = text[index]; + index++; + } + output[index] = '\0'; +} + +static void copy_exception(JSContext *context, char *output, + unsigned int capacity) +{ + JSValue exception; + const char *text; + + exception = JS_GetException(context); + text = JS_ToCString(context, exception); + copy_text(output, capacity, text ? text : "JavaScript exception"); + if (text) + JS_FreeCString(context, text); + JS_FreeValue(context, exception); +} + +#ifdef _WIN32 +static void trace_first_frame(Wm6QuickJS *host, const WCHAR *message) +{ + if (host && !host->first_frame_traced) + OutputDebugStringW(message); +} +#else +#define trace_first_frame(host, message) ((void)(host)) +#endif + +static JSValue runtime_print(JSContext *context, JSValueConst this_value, + int argument_count, JSValueConst *arguments) +{ + Wm6QuickJS *host; + const char *text; + + (void)this_value; + host = (Wm6QuickJS *)JS_GetContextOpaque(context); + if (!host || argument_count < 1) + return JS_UNDEFINED; + text = JS_ToCString(context, arguments[0]); + if (!text) + return JS_EXCEPTION; + copy_text(host->printed, sizeof(host->printed), text); + JS_FreeCString(context, text); + return JS_UNDEFINED; +} + +static int int_argument(JSContext *context, int argument_count, + JSValueConst *arguments, int index, int32_t *value) +{ + if (index >= argument_count) { + JS_ThrowTypeError(context, "missing argument %d", index); + return 0; + } + return JS_ToInt32(context, value, arguments[index]) == 0; +} + +static int uint_argument(JSContext *context, int argument_count, + JSValueConst *arguments, int index, uint32_t *value) +{ + if (index >= argument_count) { + JS_ThrowTypeError(context, "missing argument %d", index); + return 0; + } + return JS_ToUint32(context, value, arguments[index]) == 0; +} + +static int float_argument(JSContext *context, int argument_count, + JSValueConst *arguments, int index, double *value) +{ + if (index >= argument_count) { + JS_ThrowTypeError(context, "missing argument %d", index); + return 0; + } + return JS_ToFloat64(context, value, arguments[index]) == 0; +} + +static int nonnegative_uint_argument(JSContext *context, int argument_count, + JSValueConst *arguments, int index, + uint32_t *value) +{ + double raw; + + raw = 0.0; + if (!float_argument(context, argument_count, arguments, index, &raw)) + return 0; + if (raw <= 0.0) + *value = 0; + else if (raw >= 4294967295.0) + *value = 0xffffffffU; + else + *value = (uint32_t)raw; + return 1; +} + +static int string_argument(JSContext *context, int argument_count, + JSValueConst *arguments, int index, + const char **text, size_t *length) +{ + if (index >= argument_count) { + JS_ThrowTypeError(context, "missing argument %d", index); + return 0; + } + *text = JS_ToCStringLen2(context, length, arguments[index], 0); + return *text != NULL; +} + +static int bytes_argument(JSContext *context, int argument_count, + JSValueConst *arguments, int index, + const uint8_t **data, size_t *length) +{ + JSValue buffer; + JSValue direct_error; + uint8_t *base; + uint8_t *direct; + size_t offset; + size_t byte_length; + size_t bytes_per_element; + size_t buffer_length; + + if (index >= argument_count) { + JS_ThrowTypeError(context, "missing argument %d", index); + return 0; + } + direct = JS_GetArrayBuffer(context, length, arguments[index]); + if (!JS_HasException(context)) { + *data = direct; + return 1; + } + direct_error = JS_GetException(context); + JS_FreeValue(context, direct_error); + + offset = 0; + byte_length = 0; + bytes_per_element = 0; + buffer = JS_GetTypedArrayBuffer( + context, + arguments[index], + &offset, + &byte_length, + &bytes_per_element); + if (JS_IsException(buffer)) + return 0; + buffer_length = 0; + base = JS_GetArrayBuffer(context, &buffer_length, buffer); + if (JS_HasException(context)) { + JS_FreeValue(context, buffer); + return 0; + } + (void)bytes_per_element; + if (offset > buffer_length || byte_length > buffer_length - offset) { + JS_FreeValue(context, buffer); + JS_ThrowRangeError( + context, "typed array is outside its backing buffer"); + return 0; + } + *data = base ? base + offset : NULL; + *length = byte_length; + JS_FreeValue(context, buffer); + return 1; +} + +static JSValue runtime_host_operation( + JSContext *context, + JSValueConst this_value, + int argument_count, + JSValueConst *arguments, + int magic) +{ + int32_t a; + int32_t b; + int32_t c; + uint32_t ua; + uint32_t ub; + uint32_t uc; + uint32_t ud; + double da; + double db; + double dc; + double dd; + const uint8_t *bytes; + size_t byte_length; + const char *text; + size_t text_length; + + (void)this_value; + a = b = c = 0; + ua = ub = uc = ud = 0; + da = db = dc = dd = 0.0; + bytes = NULL; + byte_length = 0; + text = NULL; + text_length = 0; + + switch ((Wm6HostOperation)magic) { + case WM6_HOST_CREATE_NODE: + if (!uint_argument(context, argument_count, arguments, 0, &ua)) + return JS_EXCEPTION; + return JS_NewInt32(context, ui_create_node(ua)); + case WM6_HOST_DESTROY_NODE: + if (!int_argument(context, argument_count, arguments, 0, &a)) + return JS_EXCEPTION; + ui_destroy_node(a); + return JS_UNDEFINED; + case WM6_HOST_INSERT_BEFORE: + if (!int_argument(context, argument_count, arguments, 0, &a) || + !int_argument(context, argument_count, arguments, 1, &b) || + !int_argument(context, argument_count, arguments, 2, &c)) + return JS_EXCEPTION; + ui_insert_before(a, b, c); + return JS_UNDEFINED; + case WM6_HOST_REMOVE_CHILD: + if (!int_argument(context, argument_count, arguments, 0, &a) || + !int_argument(context, argument_count, arguments, 1, &b)) + return JS_EXCEPTION; + ui_remove_child(a, b); + return JS_UNDEFINED; + case WM6_HOST_SET_STYLE: + if (!int_argument(context, argument_count, arguments, 0, &a) || + !int_argument(context, argument_count, arguments, 1, &b)) + return JS_EXCEPTION; + ui_set_style(a, b); + return JS_UNDEFINED; + case WM6_HOST_SET_PROP: + if (!int_argument(context, argument_count, arguments, 0, &a) || + !uint_argument(context, argument_count, arguments, 1, &ua) || + !float_argument(context, argument_count, arguments, 2, &da)) + return JS_EXCEPTION; + ui_set_prop(a, ua, da); + return JS_UNDEFINED; + case WM6_HOST_SET_PROP_BATCH: + if (!bytes_argument( + context, argument_count, arguments, 0, &bytes, &byte_length)) + return JS_EXCEPTION; + ui_set_prop_batch(bytes, byte_length); + return JS_UNDEFINED; + case WM6_HOST_SET_TEXT: + case WM6_HOST_REPLACE_TEXT: + if (!int_argument(context, argument_count, arguments, 0, &a) || + !string_argument( + context, argument_count, arguments, 1, &text, &text_length)) + return JS_EXCEPTION; + if (magic == WM6_HOST_SET_TEXT) + ui_set_text(a, (const uint8_t *)text, text_length); + else + ui_replace_text(a, (const uint8_t *)text, text_length); + JS_FreeCString(context, text); + return JS_UNDEFINED; + case WM6_HOST_UPLOAD_TEXTURE: + if (!bytes_argument( + context, argument_count, arguments, 0, &bytes, &byte_length) || + !uint_argument(context, argument_count, arguments, 1, &ua) || + !uint_argument(context, argument_count, arguments, 2, &ub) || + !uint_argument(context, argument_count, arguments, 3, &uc)) + return JS_EXCEPTION; + return JS_NewInt32( + context, ui_upload_texture(bytes, byte_length, ua, ub, uc)); + case WM6_HOST_UPLOAD_IMG_ENTRY: + if (!bytes_argument( + context, argument_count, arguments, 0, &bytes, &byte_length)) + return JS_EXCEPTION; + return JS_NewInt32(context, ui_upload_img_entry(bytes, byte_length)); + case WM6_HOST_FREE_TEXTURE: + if (!int_argument(context, argument_count, arguments, 0, &a)) + return JS_EXCEPTION; + ui_free_texture(a); + return JS_UNDEFINED; + case WM6_HOST_SET_IMAGE: + if (!int_argument(context, argument_count, arguments, 0, &a) || + !int_argument(context, argument_count, arguments, 1, &b)) + return JS_EXCEPTION; + ui_set_image(a, b); + return JS_UNDEFINED; + case WM6_HOST_SET_SPRITE: + if (!int_argument(context, argument_count, arguments, 0, &a) || + !int_argument(context, argument_count, arguments, 1, &b) || + !nonnegative_uint_argument( + context, argument_count, arguments, 2, &ua) || + !nonnegative_uint_argument( + context, argument_count, arguments, 3, &ub) || + !nonnegative_uint_argument( + context, argument_count, arguments, 4, &uc)) + return JS_EXCEPTION; + ui_set_sprite(a, b, ua, ub, uc); + return JS_UNDEFINED; + case WM6_HOST_ANIMATE: + if (!int_argument(context, argument_count, arguments, 0, &a) || + !uint_argument(context, argument_count, arguments, 1, &ua) || + !float_argument(context, argument_count, arguments, 2, &da) || + !nonnegative_uint_argument( + context, argument_count, arguments, 3, &ub) || + !uint_argument(context, argument_count, arguments, 4, &uc) || + !nonnegative_uint_argument( + context, argument_count, arguments, 5, &ud)) + return JS_EXCEPTION; + return JS_NewInt32(context, ui_animate(a, ua, da, ub, uc, ud)); + case WM6_HOST_CANCEL_ANIM: + if (!int_argument(context, argument_count, arguments, 0, &a)) + return JS_EXCEPTION; + ui_cancel_anim(a); + return JS_UNDEFINED; + case WM6_HOST_SET_FOCUS: + if (!int_argument(context, argument_count, arguments, 0, &a)) + return JS_EXCEPTION; + ui_set_focus(a); + return JS_UNDEFINED; + case WM6_HOST_SET_ACTIVE: + if (!int_argument(context, argument_count, arguments, 0, &a) || + !int_argument(context, argument_count, arguments, 1, &b)) + return JS_EXCEPTION; + ui_set_active(a, b); + return JS_UNDEFINED; + case WM6_HOST_HIT_TEST: + if (!float_argument(context, argument_count, arguments, 0, &da) || + !float_argument(context, argument_count, arguments, 1, &db)) + return JS_EXCEPTION; + return JS_NewInt32(context, ui_hit_test((float)da, (float)db)); + case WM6_HOST_SET_CURSOR: + if (!int_argument(context, argument_count, arguments, 0, &a) || + !float_argument(context, argument_count, arguments, 1, &da) || + !float_argument(context, argument_count, arguments, 2, &db) || + !float_argument(context, argument_count, arguments, 3, &dc) || + !float_argument(context, argument_count, arguments, 4, &dd)) + return JS_EXCEPTION; + ui_set_cursor(a, (float)da, (float)db, (float)dc, (float)dd); + return JS_UNDEFINED; + case WM6_HOST_SET_CURSOR_POS: + if (!float_argument(context, argument_count, arguments, 0, &da) || + !float_argument(context, argument_count, arguments, 1, &db)) + return JS_EXCEPTION; + ui_set_cursor_pos((float)da, (float)db); + return JS_UNDEFINED; + case WM6_HOST_LOAD_STYLES: + case WM6_HOST_LOAD_FONT_ATLAS: + if (!bytes_argument( + context, argument_count, arguments, 0, &bytes, &byte_length)) + return JS_EXCEPTION; + if (magic == WM6_HOST_LOAD_STYLES) + return JS_NewBool(context, ui_load_styles(bytes, byte_length)); + return JS_NewBool(context, ui_load_font_atlas(bytes, byte_length)); + case WM6_HOST_MEASURE_TEXT: + if (!string_argument( + context, argument_count, arguments, 0, &text, &text_length)) + return JS_EXCEPTION; + if (!uint_argument(context, argument_count, arguments, 1, &ua)) { + JS_FreeCString(context, text); + return JS_EXCEPTION; + } + da = ui_measure_text((const uint8_t *)text, text_length, ua); + JS_FreeCString(context, text); + return JS_NewFloat64(context, da); + case WM6_HOST_DEBUG_INSPECT: + if (!int_argument(context, argument_count, arguments, 0, &a)) + return JS_EXCEPTION; + ui_debug_inspect(a); + return JS_UNDEFINED; + case WM6_HOST_DEBUG_RECT_XY: + return JS_NewInt32(context, ui_debug_rect_xy()); + case WM6_HOST_DEBUG_RECT_WH: + return JS_NewInt32(context, ui_debug_rect_wh()); + case WM6_HOST_DEBUG_PAUSE: + if (!int_argument(context, argument_count, arguments, 0, &a)) + return JS_EXCEPTION; + ui_debug_pause(a); + return JS_UNDEFINED; + case WM6_HOST_DEBUG_STEP: + ui_debug_step(); + return JS_UNDEFINED; + } + return JS_ThrowInternalError(context, "unknown PocketJS HostOp"); +} + +static int add_host_operation(JSContext *context, JSValueConst object, + const char *name, int arity, + Wm6HostOperation operation) +{ + JSValue function; + + function = JS_NewCFunctionMagic( + context, + runtime_host_operation, + name, + arity, + JS_CFUNC_generic_magic, + (int)operation); + return JS_SetPropertyStr(context, object, name, function) >= 0; +} + +static int install_core_ui(JSContext *context, JSValueConst global, + unsigned int viewport_width, + unsigned int viewport_height) +{ + JSValue ui; + JSValue viewport; + + ui = JS_NewObject(context); + if (JS_IsException(ui)) + return 0; +#define ADD_HOST(name, arity, operation) \ + if (!add_host_operation(context, ui, name, arity, operation)) goto fail + ADD_HOST("createNode", 1, WM6_HOST_CREATE_NODE); + ADD_HOST("destroyNode", 1, WM6_HOST_DESTROY_NODE); + ADD_HOST("insertBefore", 3, WM6_HOST_INSERT_BEFORE); + ADD_HOST("removeChild", 2, WM6_HOST_REMOVE_CHILD); + ADD_HOST("setStyle", 2, WM6_HOST_SET_STYLE); + ADD_HOST("setProp", 3, WM6_HOST_SET_PROP); + ADD_HOST("setPropBatch", 1, WM6_HOST_SET_PROP_BATCH); + ADD_HOST("setText", 2, WM6_HOST_SET_TEXT); + ADD_HOST("replaceText", 2, WM6_HOST_REPLACE_TEXT); + ADD_HOST("uploadTexture", 4, WM6_HOST_UPLOAD_TEXTURE); + ADD_HOST("uploadImgEntry", 1, WM6_HOST_UPLOAD_IMG_ENTRY); + ADD_HOST("freeTexture", 1, WM6_HOST_FREE_TEXTURE); + ADD_HOST("setImage", 2, WM6_HOST_SET_IMAGE); + ADD_HOST("setSprite", 5, WM6_HOST_SET_SPRITE); + ADD_HOST("animate", 6, WM6_HOST_ANIMATE); + ADD_HOST("cancelAnim", 1, WM6_HOST_CANCEL_ANIM); + ADD_HOST("setFocus", 1, WM6_HOST_SET_FOCUS); + ADD_HOST("setActive", 2, WM6_HOST_SET_ACTIVE); + ADD_HOST("hitTest", 2, WM6_HOST_HIT_TEST); + ADD_HOST("setCursor", 5, WM6_HOST_SET_CURSOR); + ADD_HOST("setCursorPos", 2, WM6_HOST_SET_CURSOR_POS); + ADD_HOST("loadStyles", 1, WM6_HOST_LOAD_STYLES); + ADD_HOST("loadFontAtlas", 1, WM6_HOST_LOAD_FONT_ATLAS); + ADD_HOST("measureText", 2, WM6_HOST_MEASURE_TEXT); + ADD_HOST("debugInspect", 1, WM6_HOST_DEBUG_INSPECT); + ADD_HOST("debugRectXY", 0, WM6_HOST_DEBUG_RECT_XY); + ADD_HOST("debugRectWH", 0, WM6_HOST_DEBUG_RECT_WH); + ADD_HOST("debugPause", 1, WM6_HOST_DEBUG_PAUSE); + ADD_HOST("debugStep", 0, WM6_HOST_DEBUG_STEP); +#undef ADD_HOST + + viewport = JS_NewObject(context); + if (JS_IsException(viewport)) + goto fail; + if (JS_SetPropertyStr( + context, viewport, "w", + JS_NewInt32(context, (int)viewport_width)) < 0 || + JS_SetPropertyStr( + context, viewport, "h", + JS_NewInt32(context, (int)viewport_height)) < 0 || + JS_SetPropertyStr(context, ui, "__viewport", viewport) < 0 || + JS_SetPropertyStr( + context, ui, "__host", + JS_NewString(context, "wm6-rust-core")) < 0 || + JS_SetPropertyStr( + context, ui, "__hostAbi", JS_NewInt32(context, 1)) < 0 || + JS_SetPropertyStr(context, global, "ui", ui) < 0) + goto fail_no_viewport; + return 1; + +fail_no_viewport: + /* Ownership of successfully assigned values has already transferred. */ + return 0; +fail: + JS_FreeValue(context, ui); + return 0; +} + +__declspec(dllexport) unsigned int __cdecl wm6_qjs_abi_version(void) +{ + return WM6_QJS_ABI_VERSION; +} + +__declspec(dllexport) wm6_qjs_handle __cdecl wm6_qjs_create( + unsigned int memory_limit, + unsigned int stack_limit, + unsigned int viewport_width, + unsigned int viewport_height, + char *error, + unsigned int error_capacity) +{ + Wm6QuickJS *host; + JSValue global; + + copy_text(error, error_capacity, ""); + host = (Wm6QuickJS *)malloc(sizeof(*host)); + if (!host) { + copy_text(error, error_capacity, "host allocation failed"); + return NULL; + } + memset(host, 0, sizeof(*host)); + host->runtime = JS_NewRuntime(); + if (!host->runtime) { + free(host); + copy_text(error, error_capacity, "JS_NewRuntime failed"); + return NULL; + } + JS_SetMemoryLimit(host->runtime, memory_limit); + JS_SetMaxStackSize(host->runtime, stack_limit); + host->context = JS_NewContext(host->runtime); + if (!host->context) { + JS_FreeRuntime(host->runtime); + free(host); + copy_text(error, error_capacity, "JS_NewContext failed"); + return NULL; + } + JS_SetContextOpaque(host->context, host); + ui_init(1); + ui_set_viewport((float)viewport_width, (float)viewport_height); + global = JS_GetGlobalObject(host->context); + if (JS_SetPropertyStr( + host->context, + global, + "print", + JS_NewCFunction(host->context, runtime_print, "print", 1)) < 0 || + !install_core_ui( + host->context, global, viewport_width, viewport_height)) { + JS_FreeValue(host->context, global); + ui_shutdown(); + JS_FreeContext(host->context); + JS_FreeRuntime(host->runtime); + free(host); + copy_text(error, error_capacity, "registering PocketJS HostOps failed"); + return NULL; + } + JS_FreeValue(host->context, global); + return (wm6_qjs_handle)host; +} + +__declspec(dllexport) int __cdecl wm6_qjs_set_pak( + wm6_qjs_handle opaque, + const unsigned char *data, + unsigned int data_length, + char *error, + unsigned int error_capacity) +{ + Wm6QuickJS *host; + JSValue global; + JSValue pack; + + host = (Wm6QuickJS *)opaque; + if (!host || (!data && data_length != 0)) + return -1; + copy_text(error, error_capacity, ""); + global = JS_GetGlobalObject(host->context); + pack = JS_NewArrayBufferCopy(host->context, data, data_length); + if (JS_IsException(pack) || + JS_SetPropertyStr(host->context, global, "__pak", pack) < 0) { + JS_FreeValue(host->context, global); + copy_exception(host->context, error, error_capacity); + return -2; + } + JS_FreeValue(host->context, global); + return 0; +} + +__declspec(dllexport) int __cdecl wm6_qjs_eval( + wm6_qjs_handle opaque, + const char *source, + unsigned int source_length, + char *output, + unsigned int output_capacity) +{ + Wm6QuickJS *host; + JSValue result; + const char *text; + + host = (Wm6QuickJS *)opaque; + if (!host || !source) + return -1; + host->printed[0] = '\0'; + copy_text(output, output_capacity, ""); + result = JS_Eval(host->context, source, source_length, + "pocketjs-wm6.js", JS_EVAL_TYPE_GLOBAL); + if (JS_IsException(result)) { + copy_exception(host->context, output, output_capacity); + JS_FreeValue(host->context, result); + return -2; + } + text = JS_ToCString(host->context, result); + copy_text(output, output_capacity, text ? text : "ok"); + if (text) + JS_FreeCString(host->context, text); + JS_FreeValue(host->context, result); + return 0; +} + +__declspec(dllexport) int __cdecl wm6_qjs_drain_jobs( + wm6_qjs_handle opaque, + char *output, + unsigned int output_capacity) +{ + Wm6QuickJS *host; + JSContext *job_context; + int count; + + host = (Wm6QuickJS *)opaque; + if (!host) + return -1; + count = 0; + while (JS_IsJobPending(host->runtime)) { + job_context = NULL; + if (JS_ExecutePendingJob(host->runtime, &job_context) < 0) { + copy_exception(job_context ? job_context : host->context, + output, output_capacity); + return -2; + } + count++; + } + copy_text(output, output_capacity, + host->printed[0] ? host->printed : "no print output"); + return count; +} + +__declspec(dllexport) const unsigned char *__cdecl wm6_qjs_frame( + wm6_qjs_handle opaque, + unsigned int buttons, + const unsigned int *touches, + unsigned int touch_count, + unsigned int *width, + unsigned int *height, + unsigned int *stride, + unsigned int *byte_length, + char *error, + unsigned int error_capacity) +{ + Wm6QuickJS *host; + JSContext *job_context; + JSValue global; + JSValue frame; + JSValue arguments[3]; + JSValue result; + const uint8_t *pixels; + size_t length; + unsigned int touch_index; + + host = (Wm6QuickJS *)opaque; + copy_text(error, error_capacity, ""); + if (!host || (!touches && touch_count != 0)) { + copy_text(error, error_capacity, "invalid frame arguments"); + return NULL; + } + trace_first_frame( + host, L"PocketJS WM6 trace: QuickJS frame call begin\r\n"); + global = JS_GetGlobalObject(host->context); + frame = JS_GetPropertyStr(host->context, global, "frame"); + if (!JS_IsFunction(host->context, frame)) { + JS_FreeValue(host->context, frame); + JS_FreeValue(host->context, global); + copy_text(error, error_capacity, "globalThis.frame is missing"); + return NULL; + } + arguments[0] = JS_NewUint32(host->context, buttons); + arguments[1] = JS_NewInt32(host->context, 0x8080); + arguments[2] = JS_NewArray(host->context); + if (touch_count > 8) + touch_count = 8; + for (touch_index = 0; touch_index < touch_count; touch_index++) { + if (JS_SetPropertyUint32( + host->context, + arguments[2], + touch_index, + JS_NewUint32(host->context, touches[touch_index])) < 0) { + JS_FreeValue(host->context, arguments[0]); + JS_FreeValue(host->context, arguments[1]); + JS_FreeValue(host->context, arguments[2]); + JS_FreeValue(host->context, frame); + JS_FreeValue(host->context, global); + copy_exception(host->context, error, error_capacity); + return NULL; + } + } + result = JS_Call( + host->context, frame, global, 3, arguments); + JS_FreeValue(host->context, arguments[0]); + JS_FreeValue(host->context, arguments[1]); + JS_FreeValue(host->context, arguments[2]); + JS_FreeValue(host->context, frame); + JS_FreeValue(host->context, global); + if (JS_IsException(result)) { + copy_exception(host->context, error, error_capacity); + JS_FreeValue(host->context, result); + return NULL; + } + JS_FreeValue(host->context, result); + trace_first_frame( + host, L"PocketJS WM6 trace: JavaScript frame complete\r\n"); + while (JS_IsJobPending(host->runtime)) { + job_context = NULL; + if (JS_ExecutePendingJob(host->runtime, &job_context) < 0) { + copy_exception( + job_context ? job_context : host->context, + error, + error_capacity); + return NULL; + } + } + trace_first_frame( + host, L"PocketJS WM6 trace: pending jobs complete\r\n"); + ui_tick(); + trace_first_frame( + host, L"PocketJS WM6 trace: Rust tick complete\r\n"); + pixels = ui_render_incremental(); + trace_first_frame( + host, L"PocketJS WM6 trace: Rust raster complete\r\n"); + length = ui_framebuffer_len(); + if (!pixels || length == 0) { + copy_text( + error, error_capacity, + "PocketJS Rust core returned an empty framebuffer"); + return NULL; + } + if (width) + *width = ui_framebuffer_width(); + if (height) + *height = ui_framebuffer_height(); + if (stride) + *stride = ui_framebuffer_stride(); + if (byte_length) + *byte_length = length > 0xffffffffU + ? 0xffffffffU + : (unsigned int)length; + host->first_frame_traced = 1; + return pixels; +} + +__declspec(dllexport) void __cdecl wm6_qjs_destroy(wm6_qjs_handle opaque) +{ + Wm6QuickJS *host; + + host = (Wm6QuickJS *)opaque; + if (!host) + return; + ui_shutdown(); + JS_FreeContext(host->context); + JS_FreeRuntime(host->runtime); + free(host); +} + +BOOL WINAPI DllMain(HANDLE module, DWORD reason, LPVOID reserved) +{ + (void)module; + (void)reason; + (void)reserved; + return TRUE; +} diff --git a/hosts/wm6/quickjs/src/wm6_math.c b/hosts/wm6/quickjs/src/wm6_math.c new file mode 100644 index 000000000..b799d1532 --- /dev/null +++ b/hosts/wm6/quickjs/src/wm6_math.c @@ -0,0 +1,24 @@ +#include + +/* + * The Windows CE CRT predates C99 and does not export fmax/fmin. Keep these + * shims in the QuickJS-owned executable so no allocator or CRT state crosses + * a future host ABI. + */ +double fmax(double left, double right) +{ + if (isnan(left)) + return right; + if (isnan(right)) + return left; + return left > right ? left : right; +} + +double fmin(double left, double right) +{ + if (isnan(left)) + return right; + if (isnan(right)) + return left; + return left < right ? left : right; +} diff --git a/hosts/wm6/quickjs/test-runtime-native.sh b/hosts/wm6/quickjs/test-runtime-native.sh new file mode 100755 index 000000000..5488f5767 --- /dev/null +++ b/hosts/wm6/quickjs/test-runtime-native.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +set -euo pipefail + +quickjs_repo="https://github.com/pocket-stack/quickjs-rs" +quickjs_rev="0fc946fb670c0c29bc0135f510bcb0f595415a61" +quickjs_version="2026-06-04" + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "${script_dir}/../../.." && pwd)" +bundle="${1:-${script_dir}/../vs2005/prebuilt/PocketJS.WM6.Demo.js}" +pak="${2:-${script_dir}/../vs2005/prebuilt/PocketJS.WM6.Demo.pak}" +viewport_width="${3:-640}" +viewport_height="${4:-480}" +work_dir="$(mktemp -d "${TMPDIR:-/tmp}/pocketjs-wm6-native-test.XXXXXX")" +trap 'rm -rf "$work_dir"' EXIT + +cargo="${CARGO:-cargo}" +cc="${CC:-cc}" +for tool in "$cargo" "$cc" git patch; do + if ! command -v "$tool" >/dev/null 2>&1; then + echo "Required tool not found: ${tool}" >&2 + exit 2 + fi +done +for asset in "$bundle" "$pak"; do + if [[ ! -f "$asset" ]]; then + echo "Required Hero asset not found: ${asset}" >&2 + exit 2 + fi +done + +if [[ -n "${WM6_QUICKJS_SOURCE:-}" ]]; then + if [[ "$(git -C "$WM6_QUICKJS_SOURCE" rev-parse HEAD)" != "$quickjs_rev" ]]; then + echo "WM6_QUICKJS_SOURCE is not at the pinned revision ${quickjs_rev}." >&2 + exit 2 + fi + cp -R "$WM6_QUICKJS_SOURCE" "${work_dir}/quickjs-rs" +else + git init -q "${work_dir}/quickjs-rs" + git -C "${work_dir}/quickjs-rs" remote add origin "$quickjs_repo" + git -C "${work_dir}/quickjs-rs" fetch -q --depth 1 origin "$quickjs_rev" + git -C "${work_dir}/quickjs-rs" checkout -q FETCH_HEAD +fi + +quickjs_dir="${work_dir}/quickjs-rs/libquickjs-sys/embed/quickjs" +patch -d "$quickjs_dir" -p1 < "${script_dir}/patches/quickjs-wm6.patch" +export CARGO_TARGET_DIR="${work_dir}/cargo" +RUSTUP_TOOLCHAIN="${RUSTUP_TOOLCHAIN:-nightly}" "$cargo" build \ + --manifest-path "${repo_root}/engine/symbian/Cargo.toml" \ + --release --no-default-features +core_archive="${CARGO_TARGET_DIR}/release/libpocketjs_symbian_core.a" + +mkdir -p "${work_dir}/obj" +common_flags=( + -std=gnu99 -O2 -funsigned-char -Wall -Wextra -Werror + "-DCONFIG_VERSION=\"${quickjs_version}\"" + "-I${script_dir}/tests" + -isystem "$quickjs_dir" + "-I${script_dir}/src" + "-I${script_dir}/../vs2005/runtime" +) +for source in cutils dtoa libregexp libunicode quickjs; do + "$cc" "${common_flags[@]}" -w -c "${quickjs_dir}/${source}.c" \ + -o "${work_dir}/obj/${source}.o" +done +"$cc" "${common_flags[@]}" -c "${script_dir}/src/runtime_dll.c" \ + -o "${work_dir}/obj/runtime_dll.o" +"$cc" "${common_flags[@]}" -c "${script_dir}/tests/runtime_smoke.c" \ + -o "${work_dir}/obj/runtime_smoke.o" + +"$cc" -o "${work_dir}/runtime-smoke" \ + "${work_dir}/obj/runtime_smoke.o" "${work_dir}/obj/runtime_dll.o" \ + "${work_dir}/obj/quickjs.o" "${work_dir}/obj/cutils.o" \ + "${work_dir}/obj/dtoa.o" "${work_dir}/obj/libregexp.o" \ + "${work_dir}/obj/libunicode.o" "$core_archive" \ + -lgcc_s -lutil -lrt -lpthread -lm -ldl +"${work_dir}/runtime-smoke" \ + "$bundle" "$pak" "$viewport_width" "$viewport_height" diff --git a/hosts/wm6/quickjs/tests/runtime_smoke.c b/hosts/wm6/quickjs/tests/runtime_smoke.c new file mode 100644 index 000000000..fd9a63ce3 --- /dev/null +++ b/hosts/wm6/quickjs/tests/runtime_smoke.c @@ -0,0 +1,256 @@ +#include +#include +#include +#include + +#include "wm6_quickjs_abi.h" + +unsigned int wm6_qjs_abi_version(void); +wm6_qjs_handle wm6_qjs_create( + unsigned int memory_limit, + unsigned int stack_limit, + unsigned int viewport_width, + unsigned int viewport_height, + char *error, + unsigned int error_capacity); +int wm6_qjs_set_pak( + wm6_qjs_handle handle, + const unsigned char *data, + unsigned int data_length, + char *error, + unsigned int error_capacity); +int wm6_qjs_eval( + wm6_qjs_handle handle, + const char *source, + unsigned int source_length, + char *output, + unsigned int output_capacity); +int wm6_qjs_drain_jobs( + wm6_qjs_handle handle, + char *output, + unsigned int output_capacity); +const unsigned char *wm6_qjs_frame( + wm6_qjs_handle handle, + unsigned int buttons, + const unsigned int *touches, + unsigned int touch_count, + unsigned int *width, + unsigned int *height, + unsigned int *stride, + unsigned int *byte_length, + char *error, + unsigned int error_capacity); +void wm6_qjs_destroy(wm6_qjs_handle handle); + +static unsigned char *read_file(const char *path, unsigned int *length) +{ + FILE *file; + long raw_length; + unsigned char *bytes; + + *length = 0; + file = fopen(path, "rb"); + if (!file) + return NULL; + if (fseek(file, 0, SEEK_END) != 0) { + fclose(file); + return NULL; + } + raw_length = ftell(file); + if (raw_length < 0 || (unsigned long)raw_length > 0xffffffffUL || + fseek(file, 0, SEEK_SET) != 0) { + fclose(file); + return NULL; + } + bytes = (unsigned char *)malloc( + raw_length > 0 ? (size_t)raw_length : 1u); + if (!bytes || + fread(bytes, 1, (size_t)raw_length, file) != + (size_t)raw_length) { + free(bytes); + fclose(file); + return NULL; + } + fclose(file); + *length = (unsigned int)raw_length; + return bytes; +} + +static uint64_t hash_bytes(const unsigned char *bytes, unsigned int length) +{ + uint64_t hash; + unsigned int index; + + hash = UINT64_C(0xcbf29ce484222325); + for (index = 0; index < length; index++) { + hash ^= bytes[index]; + hash *= UINT64_C(0x100000001b3); + } + return hash; +} + +static int parse_dimension(const char *text, unsigned int *value) +{ + char *end; + unsigned long parsed; + + if (!text || !text[0]) + return 0; + end = NULL; + parsed = strtoul(text, &end, 10); + if (!end || *end != '\0' || parsed == 0 || parsed > 1024) + return 0; + *value = (unsigned int)parsed; + return 1; +} + +static int fail( + const char *step, + const char *detail, + wm6_qjs_handle runtime, + unsigned char *bundle, + unsigned char *pak) +{ + fprintf(stderr, "%s failed: %s\n", step, detail ? detail : ""); + if (runtime) + wm6_qjs_destroy(runtime); + free(bundle); + free(pak); + return 1; +} + +int main(int argument_count, char **arguments) +{ + unsigned char *bundle; + unsigned char *pak; + unsigned int bundle_length; + unsigned int pak_length; + wm6_qjs_handle runtime; + char message[512]; + const unsigned char *pixels; + unsigned int width; + unsigned int height; + unsigned int stride; + unsigned int byte_length; + unsigned int touch; + unsigned int index; + unsigned int nonzero_alpha; + unsigned int changed_pixels; + unsigned int viewport_width; + unsigned int viewport_height; + unsigned int expected_stride; + unsigned int expected_length; + uint64_t hash; + + viewport_width = 640u; + viewport_height = 480u; + if (argument_count != 3 && argument_count != 5) { + fprintf( + stderr, + "usage: runtime_smoke BUNDLE PAK [WIDTH HEIGHT]\n"); + return 2; + } + if (argument_count == 5 && + (!parse_dimension(arguments[3], &viewport_width) || + !parse_dimension(arguments[4], &viewport_height))) { + fprintf(stderr, "viewport must be 1..1024 pixels per axis\n"); + return 2; + } + if (wm6_qjs_abi_version() != WM6_QJS_ABI_VERSION) { + fprintf(stderr, "unexpected WM6 runtime ABI\n"); + return 1; + } + bundle = read_file(arguments[1], &bundle_length); + pak = read_file(arguments[2], &pak_length); + if (!bundle || !pak) + return fail("reading Hero assets", "", NULL, bundle, pak); + + runtime = wm6_qjs_create( + 8u * 1024u * 1024u, + 256u * 1024u, + viewport_width, + viewport_height, + message, + sizeof(message)); + if (!runtime) + return fail("runtime creation", message, NULL, bundle, pak); + if (wm6_qjs_set_pak( + runtime, pak, pak_length, message, sizeof(message)) != 0) + return fail("PAK installation", message, runtime, bundle, pak); + if (wm6_qjs_eval( + runtime, + (const char *)bundle, + bundle_length, + message, + sizeof(message)) != 0) + return fail("Hero evaluation", message, runtime, bundle, pak); + if (wm6_qjs_drain_jobs(runtime, message, sizeof(message)) < 0) + return fail("initial job drain", message, runtime, bundle, pak); + + touch = 0x80000000u | + (((viewport_height / 2u) & 0x3ffu) << 10) | + ((viewport_width / 2u) & 0x3ffu); + pixels = wm6_qjs_frame( + runtime, + 0, + &touch, + 1, + &width, + &height, + &stride, + &byte_length, + message, + sizeof(message)); + if (!pixels) + return fail("first frame", message, runtime, bundle, pak); + expected_stride = viewport_width * 4u; + expected_length = expected_stride * viewport_height; + if (width != viewport_width || height != viewport_height || + stride != expected_stride || byte_length != expected_length) { + snprintf( + message, + sizeof(message), + "expected %ux%u stride=%u bytes=%u ARGB32", + viewport_width, + viewport_height, + expected_stride, + expected_length); + return fail( + "frame geometry", message, runtime, bundle, pak); + } + + nonzero_alpha = 0; + changed_pixels = 0; + for (index = 3; index < byte_length; index += 4) { + if (pixels[index] != 0) { + nonzero_alpha++; + } + if (pixels[index - 3] != pixels[0] || + pixels[index - 2] != pixels[1] || + pixels[index - 1] != pixels[2]) + changed_pixels++; + if (nonzero_alpha >= 16 && changed_pixels >= 16) + break; + } + if (nonzero_alpha < 16) + return fail( + "frame contents", "framebuffer is transparent", runtime, bundle, pak); + if (changed_pixels < 16) + return fail( + "frame contents", "framebuffer is a flat color", runtime, bundle, pak); + hash = hash_bytes(pixels, byte_length); + + wm6_qjs_destroy(runtime); + free(bundle); + free(pak); + printf( + "WM6 native runtime smoke passed: %ux%u stride=%u bytes=%u " + "fnv1a=%08x%08x\n", + width, + height, + stride, + byte_length, + (unsigned int)(hash >> 32), + (unsigned int)hash); + return 0; +} diff --git a/hosts/wm6/quickjs/tests/windows.h b/hosts/wm6/quickjs/tests/windows.h new file mode 100644 index 000000000..c40b866ac --- /dev/null +++ b/hosts/wm6/quickjs/tests/windows.h @@ -0,0 +1,19 @@ +#ifndef POCKETJS_WM6_NATIVE_TEST_WINDOWS_H +#define POCKETJS_WM6_NATIVE_TEST_WINDOWS_H + +/* + * The WM6 runtime only needs these Win32 spellings for its DLL entry point. + * Native integration tests compile the exact production source on Linux and + * replace no runtime behavior beyond the unused loader callback. + */ +typedef int BOOL; +typedef void *HANDLE; +typedef unsigned long DWORD; +typedef void *LPVOID; + +#define WINAPI +#define TRUE 1 +#define __cdecl +#define __declspec(value) + +#endif diff --git a/hosts/wm6/vs2005/.gitignore b/hosts/wm6/vs2005/.gitignore new file mode 100644 index 000000000..0143c535a --- /dev/null +++ b/hosts/wm6/vs2005/.gitignore @@ -0,0 +1,6 @@ +*.ncb +*.suo +*.user +bin/ +rebuild-release.log +obj/ diff --git a/hosts/wm6/vs2005/PocketJS.WM6.Probe.vcproj b/hosts/wm6/vs2005/PocketJS.WM6.Probe.vcproj new file mode 100644 index 000000000..c7f0ddbc1 --- /dev/null +++ b/hosts/wm6/vs2005/PocketJS.WM6.Probe.vcproj @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hosts/wm6/vs2005/PocketJS.WM6.QuickJS.vcproj b/hosts/wm6/vs2005/PocketJS.WM6.QuickJS.vcproj new file mode 100644 index 000000000..bcbb03650 --- /dev/null +++ b/hosts/wm6/vs2005/PocketJS.WM6.QuickJS.vcproj @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hosts/wm6/vs2005/PocketJS.WM6.Vapor.vcproj b/hosts/wm6/vs2005/PocketJS.WM6.Vapor.vcproj new file mode 100644 index 000000000..aa2fa420c --- /dev/null +++ b/hosts/wm6/vs2005/PocketJS.WM6.Vapor.vcproj @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hosts/wm6/vs2005/PocketJS.WM6.sln b/hosts/wm6/vs2005/PocketJS.WM6.sln new file mode 100644 index 000000000..7a4530335 --- /dev/null +++ b/hosts/wm6/vs2005/PocketJS.WM6.sln @@ -0,0 +1,37 @@ +Microsoft Visual Studio Solution File, Format Version 9.00 +# Visual Studio 2005 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PocketJS.WM6.Probe", "PocketJS.WM6.Probe.vcproj", "{64AE683E-7663-4DFD-86FC-F8FE3D972C6D}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PocketJS.WM6.Vapor", "PocketJS.WM6.Vapor.vcproj", "{970E99AC-1918-451D-A515-9C23D9C3AC65}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PocketJS.WM6.QuickJS", "PocketJS.WM6.QuickJS.vcproj", "{AA37CB32-6044-4A78-A3A5-1F58080498C8}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Windows Mobile 6 Professional SDK (ARMV4I) = Debug|Windows Mobile 6 Professional SDK (ARMV4I) + Release|Windows Mobile 6 Professional SDK (ARMV4I) = Release|Windows Mobile 6 Professional SDK (ARMV4I) + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {64AE683E-7663-4DFD-86FC-F8FE3D972C6D}.Debug|Windows Mobile 6 Professional SDK (ARMV4I).ActiveCfg = Debug|Windows Mobile 6 Professional SDK (ARMV4I) + {64AE683E-7663-4DFD-86FC-F8FE3D972C6D}.Debug|Windows Mobile 6 Professional SDK (ARMV4I).Build.0 = Debug|Windows Mobile 6 Professional SDK (ARMV4I) + {64AE683E-7663-4DFD-86FC-F8FE3D972C6D}.Debug|Windows Mobile 6 Professional SDK (ARMV4I).Deploy.0 = Debug|Windows Mobile 6 Professional SDK (ARMV4I) + {64AE683E-7663-4DFD-86FC-F8FE3D972C6D}.Release|Windows Mobile 6 Professional SDK (ARMV4I).ActiveCfg = Release|Windows Mobile 6 Professional SDK (ARMV4I) + {64AE683E-7663-4DFD-86FC-F8FE3D972C6D}.Release|Windows Mobile 6 Professional SDK (ARMV4I).Build.0 = Release|Windows Mobile 6 Professional SDK (ARMV4I) + {64AE683E-7663-4DFD-86FC-F8FE3D972C6D}.Release|Windows Mobile 6 Professional SDK (ARMV4I).Deploy.0 = Release|Windows Mobile 6 Professional SDK (ARMV4I) + {970E99AC-1918-451D-A515-9C23D9C3AC65}.Debug|Windows Mobile 6 Professional SDK (ARMV4I).ActiveCfg = Debug|Windows Mobile 6 Professional SDK (ARMV4I) + {970E99AC-1918-451D-A515-9C23D9C3AC65}.Debug|Windows Mobile 6 Professional SDK (ARMV4I).Build.0 = Debug|Windows Mobile 6 Professional SDK (ARMV4I) + {970E99AC-1918-451D-A515-9C23D9C3AC65}.Debug|Windows Mobile 6 Professional SDK (ARMV4I).Deploy.0 = Debug|Windows Mobile 6 Professional SDK (ARMV4I) + {970E99AC-1918-451D-A515-9C23D9C3AC65}.Release|Windows Mobile 6 Professional SDK (ARMV4I).ActiveCfg = Release|Windows Mobile 6 Professional SDK (ARMV4I) + {970E99AC-1918-451D-A515-9C23D9C3AC65}.Release|Windows Mobile 6 Professional SDK (ARMV4I).Build.0 = Release|Windows Mobile 6 Professional SDK (ARMV4I) + {970E99AC-1918-451D-A515-9C23D9C3AC65}.Release|Windows Mobile 6 Professional SDK (ARMV4I).Deploy.0 = Release|Windows Mobile 6 Professional SDK (ARMV4I) + {AA37CB32-6044-4A78-A3A5-1F58080498C8}.Debug|Windows Mobile 6 Professional SDK (ARMV4I).ActiveCfg = Debug|Windows Mobile 6 Professional SDK (ARMV4I) + {AA37CB32-6044-4A78-A3A5-1F58080498C8}.Debug|Windows Mobile 6 Professional SDK (ARMV4I).Build.0 = Debug|Windows Mobile 6 Professional SDK (ARMV4I) + {AA37CB32-6044-4A78-A3A5-1F58080498C8}.Debug|Windows Mobile 6 Professional SDK (ARMV4I).Deploy.0 = Debug|Windows Mobile 6 Professional SDK (ARMV4I) + {AA37CB32-6044-4A78-A3A5-1F58080498C8}.Release|Windows Mobile 6 Professional SDK (ARMV4I).ActiveCfg = Release|Windows Mobile 6 Professional SDK (ARMV4I) + {AA37CB32-6044-4A78-A3A5-1F58080498C8}.Release|Windows Mobile 6 Professional SDK (ARMV4I).Build.0 = Release|Windows Mobile 6 Professional SDK (ARMV4I) + {AA37CB32-6044-4A78-A3A5-1F58080498C8}.Release|Windows Mobile 6 Professional SDK (ARMV4I).Deploy.0 = Release|Windows Mobile 6 Professional SDK (ARMV4I) + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/hosts/wm6/vs2005/README.md b/hosts/wm6/vs2005/README.md new file mode 100644 index 000000000..3f7484a0e --- /dev/null +++ b/hosts/wm6/vs2005/README.md @@ -0,0 +1,160 @@ +# PocketJS Windows Mobile 6 VS2005 projects + +This solution contains three native Smart Device applications for the HP iPAQ +212 port: + +- `PocketJS.WM6.Probe` is the first hardware gate. It exercises the screen, + GDI, stylus, keys, timer, memory reporting, and deployment path. +- `PocketJS.WM6.Vapor` runs the repository's Pocket Vapor Todo component after + ahead-of-time compilation to portable C. It is the first application UI on + WM6, but it is not a QuickJS host and cannot load ordinary PocketJS bundles. +- `PocketJS.WM6.QuickJS` builds a VC8 host and deploys the CeGCC-built QuickJS + DLL plus the real `apps/hero` bundle (`JSX at 60 FPS.`). That DLL contains + the native ARMv4T Rust PocketJS core. QuickJS HostOps call the core's real + tree, styles, Taffy layout, animation, texture/font, and software-raster + APIs. Each incremental ARGB32 core frame is converted to RGB565, written to + a lockable DirectDraw offscreen surface, and blitted to the primary surface. + Drivers without a usable DirectDraw path fall back to a 32-bit GDI DIB + backed by the same software framebuffer. The host requests the absolute + `DMDO_90` orientation relative to the device's default portrait mode before + mounting the bundle and restores the previous mode when it exits. The + rotated `SM_CXSCREEN` and `SM_CYSCREEN` values determine both the physical + output and an integer high-DPI render scale. VGA presents a 320x240 PocketJS + framebuffer at 640x480 with a 2x scale, while QVGA remains native 320x240. + Other display sizes preserve their aspect ratio rather than scaling a fixed + 480x272 screenshot. Stylus coordinates are mapped from the physical client + to the logical viewport. The window title reports whether landscape rotation + succeeded, even without a debugger. + Once the top-level window is foreground and sized to the whole rotated + screen, the host resolves `SHFullScreen` from `aygshell.dll` at runtime to + hide the taskbar, Start icon, and SIP button. SDKs whose import library omits + that symbol use the Pocket PC `HHTaskBar` window as a fallback. The shell + chrome is restored during normal window destruction. + Pixel masks are queried from the primary surface separately after a display + rotation; a missing 16-bit mask falls back to the WM6 RGB565 layout instead + of silently converting every source color to black. + Legacy drivers get several compatible offscreen-surface capability requests; + the relaxed system-memory variant explicitly inherits the primary surface's + pixel format so that it remains lockable and suitable as a `Blt` source. + The presenter also tolerates CE drivers that omit width and height from a + successful `Lock` descriptor and reuses the validated primary pixel format + when the offscreen query omits it. + If all of them fail, the 32-bit GDI fallback copies PocketJS BGRA rows + directly instead of building both RGB565 and BGRA buffers pixel by pixel. + While DirectDraw remains active, the GDI copy is deferred until it is + actually needed, and matching RGB565 surfaces use a row copy instead of + converting every 16-bit pixel a second time. + The recurring FPS receipt also reports average QuickJS/core, framebuffer + conversion, and presentation times so emulator and device bottlenecks can + be distinguished without a profiler. + +The Probe and Vapor applications are VC8-compatible Smart Device projects +rather than desktop Win32 projects. The QuickJS deployment anchor is also a +VC8 Smart Device project; only its QuickJS DLL comes from CeGCC. The probe +exercises the OS surface that a future full PocketJS host will need: + +- ARMV4I code generation for the PXA310 device; +- a fullscreen, dynamically sized native window; +- a double-buffered GDI presentation loop; +- a `HI_RES_AWARE` executable resource so VGA devices expose native pixels; +- stylus press, drag, and release coordinates; +- hardware key events; +- runtime screen and memory reporting. + +The hardware and Vapor executables do not embed QuickJS or the PocketJS +retained UI core. The QuickJS project does: its v3 DLL ABI owns the Rust core +and forwards native HostOps, PAK loading, fixed-step frames, and framebuffer +capture; see +[`docs/WM6_IPAQ_212.md`](../../../docs/WM6_IPAQ_212.md) for the staged port. + +## Build + +The `wm6-ipaq212` restoration uses `kyokuheishin/pocketjs` branch `wm6` at +`427eb64086b50bd79dd29f4bf229e8bc7fbbf831`. The prebuilt ABI v3 DLL and Hero +assets are included; rebuilding them is optional when only compiling the +VS2005 host projects. + +Copy this entire directory to a fresh VM folder (for example `Y:\vs2005-restored`) +to avoid reusing old build products or machine-specific debugger settings. +From the **Visual Studio 2005 Command Prompt**, run `rebuild-release.cmd`. +It rebuilds all three projects for the WM6 Professional ARMV4I Release +configuration and saves the IDE output to `rebuild-release.log` here. +Return that log when reporting build errors. Successful compilation requires +all three projects to succeed and fresh Probe, Vapor, and QuickJS executables +in `bin\Release`; the checked-in standalone probe is not proof of a new build. +Keep `prebuilt` alongside the projects for DLL and guest-asset deployment. + +1. Install Visual Studio 2005 Standard or higher with **Visual C++ Smart + Device Programmability**. +2. Install Visual Studio 2005 SP1 and the relevant Vista update if the build + VM uses Vista. +3. Install **Windows Mobile 6 Professional SDK Refresh**. Microsoft maps + Windows Mobile Classic/Pocket PC devices to this SDK; the similarly named + Standard SDK is for non-touchscreen Smartphones. +4. Open `PocketJS.WM6.sln`. +5. To rebuild the native core and Hero host assets under WSL, run + `engine/wm6/build-core.sh`, `tools/build.ts hero-main`, + `hosts/wm6/quickjs/build-demo.sh`, and + `hosts/wm6/quickjs/build-runtime.sh`. The first and last commands require + the tool paths documented in their adjacent READMEs. Known-good ARM/WinCE + and JavaScript files are checked in for deployment. +6. Select `Release | Windows Mobile 6 Professional SDK (ARMV4I)`. +7. Right-click the project you want to run and choose **Set as StartUp + Project**. +8. Build and deploy through Visual Studio, or copy the corresponding executable + from `bin\Release` to the device. + +The applications have no MFC, ATL, .NET Compact Framework, or redistributable +runtime dependency. + +The real runtime project deploys `PocketJS.WM6.QuickJS.v3.dll`; the ABI suffix +prevents Windows CE from reusing an older QuickJS module still loaded by the +standalone Probe or a previous host process. After changing ABI versions, +close every old PocketJS process (or soft-reset the emulator) before deploying. +The primary executable is `PocketJS.WM6.QuickJS.exe`. VS2005 stores its remote +debugger target in a machine-specific ignored `.user` file, so an older +workspace may still request `PocketJS.WM6.QuickJS.Probe.exe`. The post-build +step deploys that name as a byte-for-byte compatibility alias of the current +runtime; it is not the old text-only Cards probe. +The VS2005 Output window reports the loaded ABI, viewport and asset sizes, the +first Rust framebuffer geometry, the actual DirectDraw surface format, and a +rolling measured FPS. Any runtime, framebuffer-copy, or DirectDraw failure +also appears in a message box instead of silently leaving a black screen. +The first frame additionally emits one-shot `trace` lines around the +JavaScript frame call, pending jobs, Rust tick, software raster, ARGB32 +conversion, and DirectDraw offscreen-surface lock. It also reports the number of +non-transparent and colored Rust pixels. If startup stalls, the final trace +line identifies the exact stage without adding per-frame logging overhead. +Opening DirectDraw does not by itself mark a frame as presentable: the initial +`WM_PAINT` fills the window through GDI and waits until the first Rust frame +has been copied. Later paint requests finish and release their GDI paint DC +before presenting. Windows CE drivers that reject direct primary-surface +locking are handled through offscreen-surface Blt, with `StretchDIBits` as a +last-resort presenter. + +## Pocket Vapor Todo controls + +The WM6 host maps the D-pad directly. Centre/Enter is A, Back is B, and the two +soft keys are Select and Start. On an emulator without those buttons, stylus +taps provide a minimal fallback: + +- top third: Up; +- middle third: Down; +- bottom-left: A; +- bottom-right: B. + +The Todo component itself uses Up/Down to select, A to toggle, B to delete, +Right to change the filter, and Start to open the editor. The checked-in +`generated\todo.gba.c` is deterministic output from +`vapor\examples\todo\todo.tsx`; the WM6 host reuses its 30×20 logical grid and +RGB555 style table. `runtime\vapor.h` and `runtime\vapor_core.c` are checked-in +copies of the repository runtime so the `vs2005` directory remains +self-contained when it is mounted as `Y:\vs2005` in the build VM. + +Press an unmapped Back/Escape key to exit. If the ROM consumes that key, stop +the application from **Settings > System > Memory > Running Programs**. + +If the probe reports `240 x 320` on a 480×640 iPAQ, the device is running an +older executable without the `HI_RES_AWARE` resource. Clean the solution, +rebuild it, and confirm that `resources\probe.rc` appears in the build log +before redeploying. diff --git a/hosts/wm6/vs2005/generated/todo.gba.c b/hosts/wm6/vs2005/generated/todo.gba.c new file mode 100644 index 000000000..9c4c49526 --- /dev/null +++ b/hosts/wm6/vs2005/generated/todo.gba.c @@ -0,0 +1,364 @@ +/* gen_app.c — GENERATED by vapor/compiler/compile.ts. DO NOT EDIT. */ +/* target: gba (30x20) */ +#define VP_GRID_W 30 +#define VP_GRID_H 20 +#define VP_STR_CAP 24 +#define VP_VIEW_CAP 32 +#include "vapor.h" + +static inline s32 vp_max(s32 a, s32 b) { return a > b ? a : b; } +static inline s32 vp_min(s32 a, s32 b) { return a < b ? a : b; } +static inline const char *vp_cstr_at(const char *const *arr, s32 n, s32 i) { return (i >= 0 && i < n) ? arr[i] : (const char *)""; } +static inline char vp_char_at(const char *s, s32 n, s32 i) { return (i >= 0 && i < n) ? s[i] : ' '; } + +typedef struct { vp_sb text; u8 done; } rec_todo; + +static rec_todo g_todos[32]; static u8 g_todos_len; +static s32 g_cursor; +static s32 g_filter; +static s32 g_editing; +static vp_sb g_draft; +static s32 g_glyph; +static u32 vp_dirty; static u32 c_valid; +static const u32 C_INVAL[6] = { 0x1fu, 0x1cu, 0x1du, 0x0u, 0x0u, 0x0u }; +static void vp_mark(u8 refIdx) { vp_dirty |= vp_bit32[refIdx]; c_valid &= ~C_INVAL[refIdx]; } + +static const char S0[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789"; +static const char S1[] = "POCKET VAPOR TODO"; +static const char S2[] = " LEFT / "; +static const char S3[] = ">"; +static const char S4[] = " "; +static const char S5[] = "["; +static const char S6[] = "X"; +static const char S7[] = "] "; +static const char S8[] = "NOTHING HERE"; +static const char S9[] = "NEW: "; +static const char S10[] = "]"; +static const char S11[] = "A:PUT B:DEL ST:SAVE SE:QUIT"; +static const char S12[] = "A:DONE B:DEL R:FILT ST:NEW"; +static const char S13[] = "SHIP POCKET VAPOR"; +static const char S14[] = "WRITE THE COMPILER"; +static const char S15[] = "RUN ON DEVICE"; + +static vp_view c_filtered_v; +static vp_view vt5; +static s32 c_remaining_v; +static rec_todo * c_current_v; +static s32 c_scroll_v; +static vp_view c_visible_v; +static const char *const A_FILTERS[3] = { "ALL", "ACTIVE", "DONE" }; + +static void c_filtered_update(void) { + if ((g_filter == 0)) { + { u8 i0; c_filtered_v.len = 0; + for (i0 = 0; i0 < g_todos_len; i0++) c_filtered_v.idx[c_filtered_v.len++] = i0; + } + } else { + if ((g_filter == 1)) { + { u8 i1; c_filtered_v.len = 0; + for (i1 = 0; i1 < g_todos_len; i1++) { + rec_todo *p2 = g_todos + (u16)(i1); + if (!p2->done) c_filtered_v.idx[c_filtered_v.len++] = i1; + } + } + } else { + { u8 i3; c_filtered_v.len = 0; + for (i3 = 0; i3 < g_todos_len; i3++) { + rec_todo *p4 = g_todos + (u16)(i3); + if (p4->done) c_filtered_v.idx[c_filtered_v.len++] = i3; + } + } + } + } +} +static const vp_view *c_filtered(void) { + if (!(c_valid & 1u)) { c_filtered_update(); c_valid |= 1u; } + return &c_filtered_v; +} + +static void c_remaining_update(void) { + { u8 i6; vt5.len = 0; + for (i6 = 0; i6 < g_todos_len; i6++) { + rec_todo *p7 = g_todos + (u16)(i6); + if (!p7->done) vt5.idx[vt5.len++] = i6; + } + } + c_remaining_v = (s32)vt5.len; +} +static s32 c_remaining(void) { + if (!(c_valid & 2u)) { c_remaining_update(); c_valid |= 2u; } + return c_remaining_v; +} + +static void c_current_update(void) { + const vp_view *v8; + rec_todo *e9; + v8 = c_filtered(); + e9 = (g_cursor >= 0 && g_cursor < (s32)v8->len) ? g_todos + (u16)(v8->idx[(u8)(g_cursor)]) : 0; + c_current_v = e9; +} +static rec_todo *c_current(void) { + if (!(c_valid & 4u)) { c_current_update(); c_valid |= 4u; } + return c_current_v; +} + +static void c_scroll_update(void) { + const vp_view *v10; + v10 = c_filtered(); + c_scroll_v = vp_max(0, vp_min(((g_cursor - 12) + 1), ((s32)v10->len - 12))); +} +static s32 c_scroll(void) { + if (!(c_valid & 8u)) { c_scroll_update(); c_valid |= 8u; } + return c_scroll_v; +} + +static void c_visible_update(void) { + const vp_view *v11; + v11 = c_filtered(); + { s32 s12 = c_scroll(), t13 = (c_scroll() + 12); u8 i14; + if (s12 < 0) s12 = 0; + if (t13 > (s32)v11->len) t13 = (s32)v11->len; + c_visible_v.len = 0; + for (i14 = (u8)s12; (s32)i14 < t13; i14++) c_visible_v.idx[c_visible_v.len++] = v11->idx[i14]; + } +} +static const vp_view *c_visible(void) { + if (!(c_valid & 16u)) { c_visible_update(); c_valid |= 16u; } + return &c_visible_v; +} + +static void fn_moveCursor(s32 p_d) { + const vp_view *v15; + v15 = c_filtered(); + { s32 nv16 = vp_max(0, vp_min((g_cursor + p_d), ((s32)v15->len - 1))); if (g_cursor != nv16) { g_cursor = nv16; vp_mark(1); } } +} + +static void km_listKeys_6(void) { + fn_moveCursor((-1)); +} + +static void km_listKeys_7(void) { + fn_moveCursor(1); +} + +static void fn_toggleDone(void) { + rec_todo *l_t_17; + l_t_17 = c_current(); + if ((l_t_17 != 0)) { + { u8 fv18 = (u8)(!l_t_17->done); if (l_t_17->done != fv18) { l_t_17->done = fv18; vp_mark(0); } } + } + fn_moveCursor(0); +} + +static void fn_deleteCurrent(void) { + rec_todo *l_t_19; + l_t_19 = c_current(); + if ((l_t_19 != 0)) { + { vp_view nv20; u8 k21; + { u8 i22; nv20.len = 0; + for (i22 = 0; i22 < g_todos_len; i22++) { + rec_todo *p23 = g_todos + (u16)(i22); + if ((p23 != l_t_19)) nv20.idx[nv20.len++] = i22; + } + } + for (k21 = 0; k21 < nv20.len; k21++) *(g_todos + (u16)k21) = *(g_todos + (u16)(nv20.idx[k21])); + g_todos_len = nv20.len; + vp_mark(0); /* new array identity always triggers */ + } + } + fn_moveCursor(0); +} + +static void fn_cycleFilter(void) { + { s32 nv24 = ((g_filter + 1) % 3); if (g_filter != nv24) { g_filter = nv24; vp_mark(2); } } + fn_moveCursor(0); +} + +static void fn_clearDone(void) { + { vp_view nv25; u8 k26; + { u8 i27; nv25.len = 0; + for (i27 = 0; i27 < g_todos_len; i27++) { + rec_todo *p28 = g_todos + (u16)(i27); + if (!p28->done) nv25.idx[nv25.len++] = i27; + } + } + for (k26 = 0; k26 < nv25.len; k26++) *(g_todos + (u16)k26) = *(g_todos + (u16)(nv25.idx[k26])); + g_todos_len = nv25.len; + vp_mark(0); /* new array identity always triggers */ + } + fn_moveCursor(0); +} + +static void fn_openEditor(void) { + { s32 nv29 = 1; if (g_editing != nv29) { g_editing = nv29; vp_mark(3); } } + { s32 nv30 = 0; if (g_glyph != nv30) { g_glyph = nv30; vp_mark(5); } } +} + +static void fn_scrubGlyph(s32 p_d) { + { s32 nv31 = (((g_glyph + p_d) + 37) % 37); if (g_glyph != nv31) { g_glyph = nv31; vp_mark(5); } } +} + +static void km_editKeys_5(void) { + fn_scrubGlyph((-1)); +} + +static void km_editKeys_4(void) { + fn_scrubGlyph(1); +} + +static void fn_putGlyph(void) { + if (((s32)(&g_draft)->len < 20)) { + { vp_sb sb32; vp_sb_reset(&sb32); + vp_sb_sb(&sb32, &g_draft); + vp_sb_ch(&sb32, vp_char_at(S0, 37, g_glyph)); + if (vp_sb_assign(&g_draft, &sb32)) vp_mark(4); + } + } +} + +static void km_editKeys_1(void) { + { vp_sb sb33; vp_sb_reset(&sb33); + { vp_sb sl; vp_sb_slice(&sl, &g_draft, 0, (-1)); vp_sb_sb(&sb33, &sl); } + if (vp_sb_assign(&g_draft, &sb33)) vp_mark(4); + } +} + +static void fn_closeEditor(void) { + { vp_sb sb35; vp_sb_reset(&sb35); + if (vp_sb_assign(&g_draft, &sb35)) vp_mark(4); + } + { s32 nv36 = 0; if (g_editing != nv36) { g_editing = nv36; vp_mark(3); } } +} + +static void fn_saveDraft(void) { + if (((s32)(&g_draft)->len > 0)) { + if (g_todos_len < 32) { + rec_todo *np = g_todos + (u16)(g_todos_len++); + { vp_sb sb34; vp_sb_reset(&sb34); + vp_sb_sb(&sb34, &g_draft); + vp_sb_assign(&np->text, &sb34); } + np->done = 0; + } else { vp_tripwires |= VP_TRIP_POOL_FULL; } + vp_mark(0); + fn_closeEditor(); + } +} + +static void eff_0(void) { + vp_row_clear(1, 2); + vp_ln_reset(); + vp_ln_int(c_remaining()); + vp_ln_str(S2); + vp_ln_str(vp_cstr_at(A_FILTERS, 3, g_filter)); + vp_ln_commit(1, 1, (u8)(2), (u8)(0)); +} + +static void eff_1(void) { + const vp_view *v39; + const vp_view *v43; + vp_row_clear(3, 15); + v39 = c_visible(); + { u8 i40; + for (i40 = 0; i40 < v39->len; i40++) { + rec_todo *t41 = g_todos + (u16)(v39->idx[i40]); + { u8 y42 = (u8)((3 + (s32)i40)); + if (y42 < 20) { + vp_ln_reset(); + vp_ln_str(((t41 == c_current()) ? S3 : S4)); + vp_ln_str(S5); + vp_ln_str((t41->done ? S6 : S4)); + vp_ln_str(S7); + vp_ln_sb(&t41->text); + vp_ln_commit(y42, 1, (u8)(((t41 == c_current()) ? 3 : (t41->done ? 4 : 0))), (u8)(0)); + } } + } } + v43 = c_filtered(); + if (((s32)v43->len == 0)) { + vp_ln_reset(); + vp_ln_str(S8); + vp_ln_commit(3, 1, (u8)(4), (u8)(0)); + } +} + +static void eff_2(void) { + vp_row_clear(17, 18); + if (g_editing) { + vp_ln_reset(); + vp_ln_str(S9); + vp_ln_sb(&g_draft); + vp_ln_str(S5); + vp_ln_ch(vp_char_at(S0, 37, g_glyph)); + vp_ln_str(S10); + vp_ln_commit(17, 1, (u8)(5), (u8)(0)); + } +} + +static void eff_3(void) { + vp_row_clear(19, 20); + vp_ln_reset(); + vp_ln_str((g_editing ? S11 : S12)); + vp_ln_commit(19, 1, (u8)(4), (u8)(0)); +} + +static void (*const KM_listKeys[10])(void) = { fn_toggleDone, fn_deleteCurrent, fn_clearDone, fn_openEditor, fn_cycleFilter, 0, km_listKeys_6, km_listKeys_7, fn_cycleFilter, 0 }; +static void (*const KM_editKeys[10])(void) = { fn_putGlyph, km_editKeys_1, fn_closeEditor, fn_saveDraft, km_editKeys_4, km_editKeys_5, 0, 0, 0, 0 }; + +void app_on_button(u8 b) { + s32 b_arg = (s32)b; + { void (*const *km37)(void) = (g_editing ? KM_editKeys : KM_listKeys); s32 bi38 = b_arg; + if (bi38 >= 0 && bi38 < 10 && km37[bi38]) km37[bi38](); } +} + +u8 app_flush(void) { + if (!vp_dirty) return 0; + if (vp_dirty & 0x5u) eff_0(); + if (vp_dirty & 0x7u) eff_1(); + if (vp_dirty & 0x38u) eff_2(); + if (vp_dirty & 0x8u) eff_3(); + vp_dirty = 0; + return 1; +} + +void app_init(void) { + g_todos_len = 3; + vp_sb_reset(&g_todos[0].text); + vp_sb_str(&g_todos[0].text, S13); + g_todos[0].done = 0; + vp_sb_reset(&g_todos[1].text); + vp_sb_str(&g_todos[1].text, S14); + g_todos[1].done = 1; + vp_sb_reset(&g_todos[2].text); + vp_sb_str(&g_todos[2].text, S15); + g_todos[2].done = 0; + g_cursor = 0; + g_filter = 0; + g_editing = 0; + vp_sb_reset(&g_draft); + g_glyph = 0; + vp_dirty = 0; c_valid = 0; + vp_ln_reset(); + vp_ln_str(S1); + vp_ln_commit(0, 0, (u8)(1), (u8)(1)); + eff_0(); + eff_1(); + eff_2(); + eff_3(); +} + +u16 app_debug_state(volatile u8 *out) { + *(volatile s32 *)(out + 0) = (s32)g_todos_len; + *(volatile s32 *)(out + 4) = (s32)g_cursor; + *(volatile s32 *)(out + 8) = (s32)g_filter; + *(volatile s32 *)(out + 12) = (s32)g_editing; + out[16] = g_draft.len; + { u8 i; for (i = 0; i < VP_STR_CAP; i++) out[16 + 1 + i] = (u8)g_draft.b[i]; } + *(volatile s32 *)(out + 44) = (s32)g_glyph; + return 48; +} + +const u8 vp_font_tiles[] = { 34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,18,33,34,34,17,17,34,34,17,17,34,34,18,33,34,34,18,33,34,34,34,34,34,34,18,33,34,34,34,34,34,18,33,17,34,18,33,17,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,18,33,17,34,18,33,17,34,17,17,17,33,18,33,17,34,17,17,17,33,18,33,17,34,18,33,17,34,34,34,34,34,34,17,34,34,18,17,17,34,17,34,34,34,18,17,33,34,34,34,17,34,17,17,33,34,34,17,34,34,34,34,34,34,34,34,34,34,17,34,18,33,17,34,17,34,34,18,33,34,34,17,34,34,18,33,18,33,17,34,18,33,34,34,34,34,34,17,33,34,18,33,17,34,34,17,33,34,18,17,18,33,17,18,17,34,17,34,17,34,18,17,18,33,34,34,34,34,18,33,34,34,18,33,34,34,17,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,18,33,34,34,17,34,34,18,33,34,34,18,33,34,34,18,33,34,34,34,17,34,34,34,18,33,34,34,34,34,34,18,33,34,34,34,17,34,34,34,18,33,34,34,18,33,34,34,18,33,34,34,17,34,34,18,33,34,34,34,34,34,34,34,34,34,34,18,33,18,33,34,17,17,34,17,17,17,17,34,17,17,34,18,33,18,33,34,34,34,34,34,34,34,34,34,34,34,34,34,17,34,34,34,17,34,34,17,17,17,34,34,17,34,34,34,17,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,17,34,34,34,17,34,34,18,33,34,34,34,34,34,34,34,34,34,34,34,34,34,34,17,17,17,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,17,34,34,34,17,34,34,34,34,34,34,34,34,18,33,34,34,17,34,34,18,33,34,34,17,34,34,18,33,34,34,17,34,34,34,33,34,34,34,34,34,34,34,18,17,17,34,17,34,18,33,17,34,17,33,17,18,17,33,17,17,18,33,17,33,18,33,18,17,17,34,34,34,34,34,34,17,34,34,18,17,34,34,34,17,34,34,34,17,34,34,34,17,34,34,34,17,34,34,17,17,17,34,34,34,34,34,18,17,33,34,17,34,17,34,34,34,17,34,34,17,33,34,18,33,34,34,17,34,17,34,17,17,17,34,34,34,34,34,18,17,33,34,17,34,17,34,34,34,17,34,34,17,33,34,34,34,17,34,17,34,17,34,18,17,33,34,34,34,34,34,34,18,17,34,34,17,17,34,18,33,17,34,17,34,17,34,17,17,17,33,34,34,17,34,34,18,17,33,34,34,34,34,17,17,17,34,17,34,34,34,17,17,33,34,34,34,17,34,34,34,17,34,17,34,17,34,18,17,33,34,34,34,34,34,34,17,33,34,18,33,34,34,17,34,34,34,17,17,33,34,17,34,17,34,17,34,17,34,18,17,33,34,34,34,34,34,17,17,17,34,17,34,17,34,34,34,17,34,34,18,33,34,34,17,34,34,34,17,34,34,34,17,34,34,34,34,34,34,18,17,33,34,17,34,17,34,17,34,17,34,18,17,33,34,17,34,17,34,17,34,17,34,18,17,33,34,34,34,34,34,18,17,33,34,17,34,17,34,17,34,17,34,18,17,17,34,34,34,17,34,34,18,33,34,18,17,34,34,34,34,34,34,34,34,34,34,34,17,34,34,34,17,34,34,34,34,34,34,34,34,34,34,34,17,34,34,34,17,34,34,34,34,34,34,34,34,34,34,34,17,34,34,34,17,34,34,34,34,34,34,34,34,34,34,34,17,34,34,34,17,34,34,18,33,34,34,34,18,33,34,34,17,34,34,18,33,34,34,17,34,34,34,18,33,34,34,34,17,34,34,34,18,33,34,34,34,34,34,34,34,34,34,34,34,34,34,17,17,17,34,34,34,34,34,34,34,34,34,17,17,17,34,34,34,34,34,34,34,34,34,18,33,34,34,34,17,34,34,34,18,33,34,34,34,17,34,34,18,33,34,34,17,34,34,18,33,34,34,34,34,34,34,18,17,33,34,17,34,17,34,34,34,17,34,34,18,33,34,34,17,34,34,34,34,34,34,34,17,34,34,34,34,34,34,18,17,17,34,17,34,18,33,17,18,17,33,17,18,17,33,17,18,17,33,17,34,34,34,18,17,33,34,34,34,34,34,34,17,34,34,18,17,33,34,17,34,17,34,17,34,17,34,17,17,17,34,17,34,17,34,17,34,17,34,34,34,34,34,17,17,17,34,18,33,18,33,18,33,18,33,18,17,17,34,18,33,18,33,18,33,18,33,17,17,17,34,34,34,34,34,34,17,17,34,18,33,18,33,17,34,34,34,17,34,34,34,17,34,34,34,18,33,18,33,34,17,17,34,34,34,34,34,17,17,33,34,18,33,17,34,18,33,18,33,18,33,18,33,18,33,18,33,18,33,17,34,17,17,33,34,34,34,34,34,17,17,17,33,18,33,34,33,18,33,33,34,18,17,33,34,18,33,33,34,18,33,34,33,17,17,17,33,34,34,34,34,17,17,17,33,18,33,34,33,18,33,33,34,18,17,33,34,18,33,33,34,18,33,34,34,17,17,34,34,34,34,34,34,34,17,17,34,18,33,18,33,17,34,34,34,17,34,34,34,17,34,17,33,18,33,18,33,34,17,17,33,34,34,34,34,17,34,17,34,17,34,17,34,17,34,17,34,17,17,17,34,17,34,17,34,17,34,17,34,17,34,17,34,34,34,34,34,18,17,33,34,34,17,34,34,34,17,34,34,34,17,34,34,34,17,34,34,34,17,34,34,18,17,33,34,34,34,34,34,34,18,17,33,34,34,17,34,34,34,17,34,34,34,17,34,17,34,17,34,17,34,17,34,18,17,33,34,34,34,34,34,17,33,18,33,18,33,18,33,18,33,17,34,18,17,33,34,18,33,17,34,18,33,18,33,17,33,18,33,34,34,34,34,17,17,34,34,18,33,34,34,18,33,34,34,18,33,34,34,18,33,34,33,18,33,18,33,17,17,17,33,34,34,34,34,17,34,18,33,17,33,17,33,17,17,17,33,17,17,17,33,17,18,18,33,17,34,18,33,17,34,18,33,34,34,34,34,17,34,18,33,17,33,18,33,17,17,18,33,17,18,17,33,17,34,17,33,17,34,18,33,17,34,18,33,34,34,34,34,34,17,33,34,18,33,17,34,17,34,18,33,17,34,18,33,17,34,18,33,18,33,17,34,34,17,33,34,34,34,34,34,17,17,17,34,18,33,18,33,18,33,18,33,18,17,17,34,18,33,34,34,18,33,34,34,17,17,34,34,34,34,34,34,18,17,33,34,17,34,17,34,17,34,17,34,17,34,17,34,17,18,17,34,18,17,33,34,34,18,17,34,34,34,34,34,17,17,17,34,18,33,18,33,18,33,18,33,18,17,17,34,18,33,17,34,18,33,18,33,17,33,18,33,34,34,34,34,18,17,33,34,17,34,17,34,17,33,34,34,18,17,34,34,34,18,17,34,17,34,17,34,18,17,33,34,34,34,34,34,17,17,17,34,33,17,18,34,34,17,34,34,34,17,34,34,34,17,34,34,34,17,34,34,18,17,33,34,34,34,34,34,17,34,17,34,17,34,17,34,17,34,17,34,17,34,17,34,17,34,17,34,17,34,17,34,17,17,17,34,34,34,34,34,17,34,17,34,17,34,17,34,17,34,17,34,17,34,17,34,17,34,17,34,18,17,33,34,34,17,34,34,34,34,34,34,17,34,18,33,17,34,18,33,17,34,18,33,17,18,18,33,17,17,17,33,17,33,17,33,17,34,18,33,34,34,34,34,17,34,18,33,17,34,18,33,18,33,17,34,34,17,33,34,34,17,33,34,18,33,17,34,17,34,18,33,34,34,34,34,17,34,17,34,17,34,17,34,17,34,17,34,18,17,33,34,34,17,34,34,34,17,34,34,18,17,33,34,34,34,34,34,17,17,17,33,17,34,18,33,33,34,17,34,34,18,33,34,34,17,34,33,18,33,18,33,17,17,17,33,34,34,34,34,18,17,33,34,18,33,34,34,18,33,34,34,18,33,34,34,18,33,34,34,18,33,34,34,18,17,33,34,34,34,34,34,17,34,34,34,18,33,34,34,34,17,34,34,34,18,33,34,34,34,17,34,34,34,18,33,34,34,34,33,34,34,34,34,18,17,33,34,34,18,33,34,34,18,33,34,34,18,33,34,34,18,33,34,34,18,33,34,18,17,33,34,34,34,34,34,34,18,34,34,34,17,33,34,18,33,17,34,17,34,18,33,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,17,17,17,17,34,17,34,34,34,17,34,34,34,18,33,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,18,17,33,34,34,34,17,34,18,17,17,34,17,34,17,34,18,17,18,33,34,34,34,34,17,33,34,34,18,33,34,34,18,33,34,34,18,17,17,34,18,33,18,33,18,33,18,33,17,18,17,34,34,34,34,34,34,34,34,34,34,34,34,34,18,17,33,34,17,34,17,34,17,34,34,34,17,34,17,34,18,17,33,34,34,34,34,34,34,18,17,34,34,34,17,34,34,34,17,34,18,17,17,34,17,34,17,34,17,34,17,34,18,17,18,33,34,34,34,34,34,34,34,34,34,34,34,34,18,17,33,34,17,34,17,34,17,17,17,34,17,34,34,34,18,17,33,34,34,34,34,34,34,17,33,34,18,33,17,34,18,33,34,34,17,17,34,34,18,33,34,34,18,33,34,34,17,17,34,34,34,34,34,34,34,34,34,34,34,34,34,34,18,17,18,33,17,34,17,34,17,34,17,34,18,17,17,34,34,34,17,34,17,17,33,34,17,33,34,34,18,33,34,34,18,33,17,34,18,17,18,33,18,33,18,33,18,33,18,33,17,33,18,33,34,34,34,34,34,17,34,34,34,34,34,34,18,17,34,34,34,17,34,34,34,17,34,34,34,17,34,34,18,17,33,34,34,34,34,34,34,34,17,34,34,34,34,34,34,34,17,34,34,34,17,34,34,34,17,34,17,34,17,34,17,34,17,34,18,17,33,34,17,33,34,34,18,33,34,34,18,33,18,33,18,33,17,34,18,17,33,34,18,33,17,34,17,33,18,33,34,34,34,34,18,17,34,34,34,17,34,34,34,17,34,34,34,17,34,34,34,17,34,34,34,17,34,34,18,17,33,34,34,34,34,34,34,34,34,34,34,34,34,34,17,34,17,34,17,17,17,33,17,17,17,33,17,18,18,33,17,34,18,33,34,34,34,34,34,34,34,34,34,34,34,34,17,17,33,34,17,34,17,34,17,34,17,34,17,34,17,34,17,34,17,34,34,34,34,34,34,34,34,34,34,34,34,34,18,17,33,34,17,34,17,34,17,34,17,34,17,34,17,34,18,17,33,34,34,34,34,34,34,34,34,34,34,34,34,34,17,18,17,34,18,33,18,33,18,33,18,33,18,17,17,34,18,33,34,34,17,17,34,34,34,34,34,34,34,34,34,34,18,17,18,33,17,34,17,34,17,34,17,34,18,17,17,34,34,34,17,34,34,18,17,33,34,34,34,34,34,34,34,34,17,18,17,34,18,17,18,33,18,33,18,33,18,33,34,34,17,17,34,34,34,34,34,34,34,34,34,34,34,34,34,34,18,17,17,34,17,34,34,34,18,17,33,34,34,34,17,34,17,17,33,34,34,34,34,34,34,18,34,34,34,17,34,34,18,17,17,34,34,17,34,34,34,17,34,34,34,17,18,34,34,18,33,34,34,34,34,34,34,34,34,34,34,34,34,34,17,34,17,34,17,34,17,34,17,34,17,34,17,34,17,34,18,17,18,33,34,34,34,34,34,34,34,34,34,34,34,34,17,34,17,34,17,34,17,34,17,34,17,34,18,17,33,34,34,17,34,34,34,34,34,34,34,34,34,34,34,34,34,34,17,34,18,33,17,18,18,33,17,17,17,33,17,17,17,33,18,33,17,34,34,34,34,34,34,34,34,34,34,34,34,34,17,34,18,33,18,33,17,34,34,17,33,34,18,33,17,34,17,34,18,33,34,34,34,34,34,34,34,34,34,34,34,34,17,34,17,34,17,34,17,34,17,34,17,34,18,17,17,34,34,34,17,34,17,17,33,34,34,34,34,34,34,34,34,34,17,17,17,34,33,18,33,34,34,17,34,34,18,33,18,34,17,17,17,34,34,34,34,34,34,18,17,34,34,17,34,34,34,17,34,34,17,33,34,34,34,17,34,34,34,17,34,34,34,18,17,34,34,34,34,34,34,34,34,34,34,18,33,34,34,18,33,34,34,34,34,34,34,18,33,34,34,18,33,34,34,18,33,34,34,34,34,34,17,33,34,34,34,17,34,34,34,17,34,34,34,18,17,34,34,17,34,34,34,17,34,34,17,33,34,34,34,34,34,34,18,17,18,33,17,18,17,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34 }; +const u16 vp_palettes[] = { 0,31676,4162,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2048,17122,0,0,0,0,0,0,0,0,0,0,0,0,0,0,20294,4162,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2048,32734,0,0,0,0,0,0,0,0,0,0,0,0,0,0,17868,4162,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2048,10079,0,0,0,0,0,0,0,0,0,0,0,0,0 }; +const u8 vp_palette_count = 6; +const u16 vp_backdrop = 4162; +const u8 vp_pal_style[6] = { 0,1,2,3,4,5 }; +const char vp_app_title[] = "VAPOR TODO"; diff --git a/hosts/wm6/vs2005/prebuilt/.gitignore b/hosts/wm6/vs2005/prebuilt/.gitignore new file mode 100644 index 000000000..a8dca6f3e --- /dev/null +++ b/hosts/wm6/vs2005/prebuilt/.gitignore @@ -0,0 +1 @@ +*.obj diff --git a/hosts/wm6/vs2005/prebuilt/PocketJS.WM6.Cards.js b/hosts/wm6/vs2005/prebuilt/PocketJS.WM6.Cards.js new file mode 100644 index 000000000..c25129203 --- /dev/null +++ b/hosts/wm6/vs2005/prebuilt/PocketJS.WM6.Cards.js @@ -0,0 +1,3853 @@ +/* PocketJS WM6 bootstrap + real apps/cards bundle. */ +(() => { + let nextNode = 2; + let nextAnim = 1; + let focused = 0; + const nodes = new Map(); + nodes.set(1, { id: 1, type: 0, style: -1, text: "", parent: 0, children: [] }); + + const node = (id) => nodes.get(id); + const detach = (id) => { + for (const parent of nodes.values()) { + const at = parent.children.indexOf(id); + if (at >= 0) parent.children.splice(at, 1); + } + const child = node(id); + if (child) child.parent = 0; + }; + + globalThis.ui = { + __host: "wm6", + __hostAbi: 1, + __textures: {}, + __viewport: { w: 480, h: 272 }, + createNode(type) { + const id = nextNode++; + nodes.set(id, { id, type, style: -1, text: "", parent: 0, children: [] }); + return id; + }, + destroyNode(id) { + detach(id); + nodes.delete(id); + }, + insertBefore(parentId, childId, anchor) { + const parent = node(parentId); + if (!parent) return; + detach(childId); + const at = anchor ? parent.children.indexOf(anchor) : -1; + if (at >= 0) parent.children.splice(at, 0, childId); + else parent.children.push(childId); + const child = node(childId); + if (child) child.parent = parentId; + }, + removeChild(_parent, child) { detach(child); }, + setStyle(id, style) { const n = node(id); if (n) n.style = style; }, + setProp() {}, + setText(id, text) { const n = node(id); if (n) n.text = String(text); }, + replaceText(id, text) { this.setText(id, text); }, + uploadTexture() { return -1; }, + setImage() {}, + setSprite() {}, + animate() { return nextAnim++; }, + cancelAnim() {}, + setFocus(id) { focused = id; }, + setActive() {}, + measureText(text, fontSlot) { + const width = fontSlot === 12 ? 13 : fontSlot === 8 ? 8 : 7; + return String(text).length * width; + } + }; + + globalThis.__wm6Snapshot = () => { + const lines = ["PocketJS Cards (real bundle)", "viewport 480x272", ""]; + const visit = (id, depth) => { + const n = node(id); + if (!n) return; + if (n.text) lines.push(" ".repeat(depth) + n.text); + for (const child of n.children) visit(child, depth + 1); + }; + visit(1, 0); + return lines.join("\n"); + }; + + globalThis.__wm6DrawList = () => { + const out = ["B|248|250|252"]; + const safe = (text) => String(text).replace(/[|\r\n]/g, " "); + const text = (x, y, slot, r, g, b, value) => + out.push(`T|${x}|${y}|${slot}|${r}|${g}|${b}|${safe(value)}`); + const rect = (x, y, w, h, r, g, b) => + out.push(`R|${x}|${y}|${w}|${h}|${r}|${g}|${b}`); + let cardIndex = 0; + const effectiveStyle = (n) => { + let current = n; + while (current) { + if (current.style >= 0) return current.style; + current = node(current.parent); + } + return -1; + }; + + for (const n of nodes.values()) { + if (!n.text) continue; + const style = effectiveStyle(n); + if (style === 18) text(16, 18, 0, 37, 99, 235, n.text); + else if (style === 19) text(16, 35, 12, 15, 23, 42, n.text); + else if (style === 20 && n.text === "3 MODULES") + text(398, 43, 0, 100, 116, 139, n.text); + else if (style === 20) + text(16, 250, 0, 100, 116, 139, n.text); + } + for (const n of nodes.values()) { + if (n.style !== 0 && n.style !== 3 && n.style !== 6) continue; + const x = 16 + cardIndex * 148; + const isFocused = n.id === focused; + const accent = n.style === 0 ? [59, 130, 246] + : n.style === 3 ? [16, 185, 129] : [245, 158, 11]; + rect(x, 76, 136, 82, isFocused ? 239 : 255, + isFocused ? 246 : 255, isFocused ? 255 : 255); + rect(x, 76, 136, 4, accent[0], accent[1], accent[2]); + const labels = []; + const collect = (id) => { + const child = node(id); + if (!child) return; + if (child.text) labels.push(child.text); + for (const nested of child.children) collect(nested); + }; + collect(n.id); + if (labels[0]) text(x + 12, 91, 8, 15, 23, 42, labels[0]); + if (labels[1]) text(x + 12, 115, 0, 71, 85, 105, labels[1]); + cardIndex++; + } + for (const n of nodes.values()) { + if (n.style !== 9) continue; + rect(16, 174, 448, 54, 255, 255, 255); + const labels = []; + const collect = (id) => { + const child = node(id); + if (!child) return; + if (child.text) labels.push(child.text); + for (const nested of child.children) collect(nested); + }; + collect(n.id); + if (labels[0]) text(34, 184, 8, 15, 23, 42, labels[0]); + if (labels[1]) text(34, 204, 0, 71, 85, 105, labels[1]); + } + return out.join("\n"); + }; +})(); +(() => { + // node_modules/solid-js/dist/solid.js + var sharedConfig = { + context: undefined, + registry: undefined, + effects: undefined, + done: false, + getContextId() { + return getContextId(this.context.count); + }, + getNextContextId() { + return getContextId(this.context.count++); + } + }; + function getContextId(count) { + const num = String(count), len = num.length - 1; + return sharedConfig.context.id + (len ? String.fromCharCode(96 + len) : "") + num; + } + function setHydrateContext(context) { + sharedConfig.context = context; + } + function nextHydrateContext() { + return { + ...sharedConfig.context, + id: sharedConfig.getNextContextId(), + count: 0 + }; + } + var IS_DEV = false; + var equalFn = (a, b) => a === b; + var $PROXY = Symbol("solid-proxy"); + var SUPPORTS_PROXY = typeof Proxy === "function"; + var $TRACK = Symbol("solid-track"); + var $DEVCOMP = Symbol("solid-dev-component"); + var signalOptions = { + equals: equalFn + }; + var ERROR = null; + var runEffects = runQueue; + var STALE = 1; + var PENDING = 2; + var UNOWNED = { + owned: null, + cleanups: null, + context: null, + owner: null + }; + var Owner = null; + var Transition = null; + var Scheduler = null; + var ExternalSourceConfig = null; + var Listener = null; + var Updates = null; + var Effects = null; + var ExecCount = 0; + function createRoot(fn, detachedOwner) { + const listener = Listener, owner = Owner, unowned = fn.length === 0, current = detachedOwner === undefined ? owner : detachedOwner, root = unowned ? UNOWNED : { + owned: null, + cleanups: null, + context: current ? current.context : null, + owner: current + }, updateFn = unowned ? fn : () => fn(() => untrack(() => cleanNode(root))); + Owner = root; + Listener = null; + try { + return runUpdates(updateFn, true); + } finally { + Listener = listener; + Owner = owner; + } + } + function createSignal(value, options) { + options = options ? Object.assign({}, signalOptions, options) : signalOptions; + const s = { + value, + observers: null, + observerSlots: null, + comparator: options.equals || undefined + }; + const setter = (value2) => { + if (typeof value2 === "function") { + if (Transition && Transition.running && Transition.sources.has(s)) + value2 = value2(s.tValue); + else + value2 = value2(s.value); + } + return writeSignal(s, value2); + }; + return [readSignal.bind(s), setter]; + } + function createRenderEffect(fn, value, options) { + const c = createComputation(fn, value, false, STALE); + if (Scheduler && Transition && Transition.running) + Updates.push(c); + else + updateComputation(c); + } + function createEffect(fn, value, options) { + runEffects = runUserEffects; + const c = createComputation(fn, value, false, STALE), s = SuspenseContext && useContext(SuspenseContext); + if (s) + c.suspense = s; + if (!options || !options.render) + c.user = true; + Effects ? Effects.push(c) : updateComputation(c); + } + function createMemo(fn, value, options) { + options = options ? Object.assign({}, signalOptions, options) : signalOptions; + const c = createComputation(fn, value, true, 0); + c.observers = null; + c.observerSlots = null; + c.comparator = options.equals || undefined; + if (Scheduler && Transition && Transition.running) { + c.tState = STALE; + Updates.push(c); + } else + updateComputation(c); + return readSignal.bind(c); + } + function untrack(fn) { + if (!ExternalSourceConfig && Listener === null) + return fn(); + const listener = Listener; + Listener = null; + try { + if (ExternalSourceConfig) + return ExternalSourceConfig.untrack(fn); + return fn(); + } finally { + Listener = listener; + } + } + function onMount(fn) { + createEffect(() => untrack(fn)); + } + function onCleanup(fn) { + if (Owner === null) + ; + else if (Owner.cleanups === null) + Owner.cleanups = [fn]; + else + Owner.cleanups.push(fn); + return fn; + } + function startTransition(fn) { + if (Transition && Transition.running) { + fn(); + return Transition.done; + } + const l = Listener; + const o = Owner; + return Promise.resolve().then(() => { + Listener = l; + Owner = o; + let t; + if (Scheduler || SuspenseContext) { + t = Transition || (Transition = { + sources: new Set, + effects: [], + promises: new Set, + disposed: new Set, + queue: new Set, + running: true + }); + t.done || (t.done = new Promise((res) => t.resolve = res)); + t.running = true; + } + runUpdates(fn, false); + Listener = Owner = null; + return t ? t.done : undefined; + }); + } + var [transPending, setTransPending] = /* @__PURE__ */ createSignal(false); + function useContext(context) { + let value; + return Owner && Owner.context && (value = Owner.context[context.id]) !== undefined ? value : context.defaultValue; + } + var SuspenseContext; + function readSignal() { + const runningTransition = Transition && Transition.running; + if (this.sources && (runningTransition ? this.tState : this.state)) { + if ((runningTransition ? this.tState : this.state) === STALE) + updateComputation(this); + else { + const updates = Updates; + Updates = null; + runUpdates(() => lookUpstream(this), false); + Updates = updates; + } + } + if (Listener) { + const observers = this.observers; + if (!observers || observers[observers.length - 1] !== Listener) { + const sSlot = observers ? observers.length : 0; + if (!Listener.sources) { + Listener.sources = [this]; + Listener.sourceSlots = [sSlot]; + } else { + Listener.sources.push(this); + Listener.sourceSlots.push(sSlot); + } + if (!observers) { + this.observers = [Listener]; + this.observerSlots = [Listener.sources.length - 1]; + } else { + observers.push(Listener); + this.observerSlots.push(Listener.sources.length - 1); + } + } + } + if (runningTransition && Transition.sources.has(this)) + return this.tValue; + return this.value; + } + function writeSignal(node, value, isComp) { + let current = Transition && Transition.running && Transition.sources.has(node) ? node.tValue : node.value; + if (!node.comparator || !node.comparator(current, value)) { + if (Transition) { + const TransitionRunning = Transition.running; + if (TransitionRunning || !isComp && Transition.sources.has(node)) { + Transition.sources.add(node); + node.tValue = value; + } + if (!TransitionRunning) + node.value = value; + } else + node.value = value; + if (node.observers && node.observers.length) { + runUpdates(() => { + for (let i = 0;i < node.observers.length; i += 1) { + const o = node.observers[i]; + const TransitionRunning = Transition && Transition.running; + if (TransitionRunning && Transition.disposed.has(o)) + continue; + if (TransitionRunning ? !o.tState : !o.state) { + if (o.pure) + Updates.push(o); + else + Effects.push(o); + if (o.observers) + markDownstream(o); + } + if (!TransitionRunning) + o.state = STALE; + else + o.tState = STALE; + } + if (Updates.length > 1e6) { + Updates = []; + if (IS_DEV) + ; + throw new Error; + } + }, false); + } + } + return value; + } + function updateComputation(node) { + if (!node.fn) + return; + cleanNode(node); + const time = ExecCount; + runComputation(node, Transition && Transition.running && Transition.sources.has(node) ? node.tValue : node.value, time); + if (Transition && !Transition.running && Transition.sources.has(node)) { + queueMicrotask(() => { + runUpdates(() => { + Transition && (Transition.running = true); + Listener = Owner = node; + runComputation(node, node.tValue, time); + Listener = Owner = null; + }, false); + }); + } + } + function runComputation(node, value, time) { + let nextValue; + const owner = Owner, listener = Listener; + Listener = Owner = node; + try { + nextValue = node.fn(value); + } catch (err) { + if (node.pure) { + if (Transition && Transition.running) { + node.tState = STALE; + node.tOwned && node.tOwned.forEach(cleanNode); + node.tOwned = undefined; + } else { + node.state = STALE; + node.owned && node.owned.forEach(cleanNode); + node.owned = null; + } + } + node.updatedAt = time + 1; + return handleError(err); + } finally { + Listener = listener; + Owner = owner; + } + if (!node.updatedAt || node.updatedAt <= time) { + if (node.updatedAt != null && "observers" in node) { + writeSignal(node, nextValue, true); + } else if (Transition && Transition.running && node.pure) { + if (!Transition.sources.has(node)) + node.value = nextValue; + Transition.sources.add(node); + node.tValue = nextValue; + } else + node.value = nextValue; + node.updatedAt = time; + } + } + function createComputation(fn, init, pure, state = STALE, options) { + const c = { + fn, + state, + updatedAt: null, + owned: null, + sources: null, + sourceSlots: null, + cleanups: null, + value: init, + owner: Owner, + context: Owner ? Owner.context : null, + pure + }; + if (Transition && Transition.running) { + c.state = 0; + c.tState = state; + } + if (Owner === null) + ; + else if (Owner !== UNOWNED) { + if (Transition && Transition.running && Owner.pure) { + if (!Owner.tOwned) + Owner.tOwned = [c]; + else + Owner.tOwned.push(c); + } else { + if (!Owner.owned) + Owner.owned = [c]; + else + Owner.owned.push(c); + } + } + if (ExternalSourceConfig && c.fn) { + const sourceFn = c.fn; + const [track, trigger] = createSignal(undefined, { + equals: false + }); + const ordinary = ExternalSourceConfig.factory(sourceFn, trigger); + onCleanup(() => ordinary.dispose()); + let inTransition; + const triggerInTransition = () => startTransition(trigger).then(() => { + if (inTransition) { + inTransition.dispose(); + inTransition = undefined; + } + }); + c.fn = (x) => { + track(); + if (Transition && Transition.running) { + if (!inTransition) + inTransition = ExternalSourceConfig.factory(sourceFn, triggerInTransition); + return inTransition.track(x); + } + return ordinary.track(x); + }; + } + return c; + } + function runTop(node) { + const runningTransition = Transition && Transition.running; + if ((runningTransition ? node.tState : node.state) === 0) + return; + if ((runningTransition ? node.tState : node.state) === PENDING) + return lookUpstream(node); + if (node.suspense && untrack(node.suspense.inFallback)) + return node.suspense.effects.push(node); + const ancestors = [node]; + while ((node = node.owner) && (!node.updatedAt || node.updatedAt < ExecCount)) { + if (runningTransition && Transition.disposed.has(node)) + return; + if (runningTransition ? node.tState : node.state) + ancestors.push(node); + } + for (let i = ancestors.length - 1;i >= 0; i--) { + node = ancestors[i]; + if (runningTransition) { + let top = node, prev = ancestors[i + 1]; + while ((top = top.owner) && top !== prev) { + if (Transition.disposed.has(top)) + return; + } + } + if ((runningTransition ? node.tState : node.state) === STALE) { + updateComputation(node); + } else if ((runningTransition ? node.tState : node.state) === PENDING) { + const updates = Updates; + Updates = null; + runUpdates(() => lookUpstream(node, ancestors[0]), false); + Updates = updates; + } + } + } + function runUpdates(fn, init) { + if (Updates) + return fn(); + let wait = false; + if (!init) + Updates = []; + if (Effects) + wait = true; + else + Effects = []; + ExecCount++; + try { + const res = fn(); + completeUpdates(wait); + return res; + } catch (err) { + if (!wait) + Effects = null; + Updates = null; + handleError(err); + } + } + function completeUpdates(wait) { + if (Updates) { + if (Scheduler && Transition && Transition.running) + scheduleQueue(Updates); + else + runQueue(Updates); + Updates = null; + } + if (wait) + return; + let res; + if (Transition) { + if (!Transition.promises.size && !Transition.queue.size) { + const sources = Transition.sources; + const disposed = Transition.disposed; + Effects.push.apply(Effects, Transition.effects); + res = Transition.resolve; + for (const e2 of Effects) { + "tState" in e2 && (e2.state = e2.tState); + delete e2.tState; + } + Transition = null; + runUpdates(() => { + for (const d of disposed) + cleanNode(d); + for (const v of sources) { + v.value = v.tValue; + if (v.owned) { + for (let i = 0, len = v.owned.length;i < len; i++) + cleanNode(v.owned[i]); + } + if (v.tOwned) + v.owned = v.tOwned; + delete v.tValue; + delete v.tOwned; + v.tState = 0; + } + setTransPending(false); + }, false); + } else if (Transition.running) { + Transition.running = false; + Transition.effects.push.apply(Transition.effects, Effects); + Effects = null; + setTransPending(true); + return; + } + } + const e = Effects; + Effects = null; + if (e.length) + runUpdates(() => runEffects(e), false); + if (res) + res(); + } + function runQueue(queue) { + for (let i = 0;i < queue.length; i++) + runTop(queue[i]); + } + function scheduleQueue(queue) { + for (let i = 0;i < queue.length; i++) { + const item = queue[i]; + const tasks = Transition.queue; + if (!tasks.has(item)) { + tasks.add(item); + Scheduler(() => { + tasks.delete(item); + runUpdates(() => { + Transition.running = true; + runTop(item); + }, false); + Transition && (Transition.running = false); + }); + } + } + } + function runUserEffects(queue) { + let i, userLength = 0; + for (i = 0;i < queue.length; i++) { + const e = queue[i]; + if (!e.user) + runTop(e); + else + queue[userLength++] = e; + } + if (sharedConfig.context) { + if (sharedConfig.count) { + sharedConfig.effects || (sharedConfig.effects = []); + sharedConfig.effects.push(...queue.slice(0, userLength)); + return; + } + setHydrateContext(); + } + if (sharedConfig.effects && (sharedConfig.done || !sharedConfig.count)) { + queue = [...sharedConfig.effects, ...queue]; + userLength += sharedConfig.effects.length; + delete sharedConfig.effects; + } + for (i = 0;i < userLength; i++) + runTop(queue[i]); + } + function lookUpstream(node, ignore) { + const runningTransition = Transition && Transition.running; + if (runningTransition) + node.tState = 0; + else + node.state = 0; + for (let i = 0;i < node.sources.length; i += 1) { + const source = node.sources[i]; + if (source.sources) { + const state = runningTransition ? source.tState : source.state; + if (state === STALE) { + if (source !== ignore && (!source.updatedAt || source.updatedAt < ExecCount)) + runTop(source); + } else if (state === PENDING) + lookUpstream(source, ignore); + } + } + } + function markDownstream(node) { + const runningTransition = Transition && Transition.running; + for (let i = 0;i < node.observers.length; i += 1) { + const o = node.observers[i]; + if (runningTransition ? !o.tState : !o.state) { + if (runningTransition) + o.tState = PENDING; + else + o.state = PENDING; + if (o.pure) + Updates.push(o); + else + Effects.push(o); + o.observers && markDownstream(o); + } + } + } + function cleanNode(node) { + let i; + if (node.sources) { + while (node.sources.length) { + const source = node.sources.pop(), index = node.sourceSlots.pop(), obs = source.observers; + if (obs && obs.length) { + const n = obs.pop(), s = source.observerSlots.pop(); + if (index < obs.length) { + n.sourceSlots[s] = index; + obs[index] = n; + source.observerSlots[index] = s; + } + } + } + } + if (node.tOwned) { + for (i = node.tOwned.length - 1;i >= 0; i--) + cleanNode(node.tOwned[i]); + delete node.tOwned; + } + if (Transition && Transition.running && node.pure) { + reset(node, true); + } else if (node.owned) { + for (i = node.owned.length - 1;i >= 0; i--) + cleanNode(node.owned[i]); + node.owned = null; + } + if (node.cleanups) { + for (i = node.cleanups.length - 1;i >= 0; i--) + node.cleanups[i](); + node.cleanups = null; + } + if (Transition && Transition.running) + node.tState = 0; + else + node.state = 0; + } + function reset(node, top) { + if (!top) { + node.tState = 0; + Transition.disposed.add(node); + } + if (node.owned) { + for (let i = 0;i < node.owned.length; i++) + reset(node.owned[i]); + } + } + function castError(err) { + if (err instanceof Error) + return err; + return new Error(typeof err === "string" ? err : "Unknown error", { + cause: err + }); + } + function runErrors(err, fns, owner) { + try { + for (const f of fns) + f(err); + } catch (e) { + handleError(e, owner && owner.owner || null); + } + } + function handleError(err, owner = Owner) { + const fns = ERROR && owner && owner.context && owner.context[ERROR]; + const error = castError(err); + if (!fns) + throw error; + if (Effects) + Effects.push({ + fn() { + runErrors(error, fns, owner); + }, + state: STALE + }); + else + runErrors(error, fns, owner); + } + var FALLBACK = Symbol("fallback"); + var hydrationEnabled = false; + function createComponent(Comp, props) { + if (hydrationEnabled) { + if (sharedConfig.context) { + const c = sharedConfig.context; + setHydrateContext(nextHydrateContext()); + const r = untrack(() => Comp(props || {})); + setHydrateContext(c); + return r; + } + } + return untrack(() => Comp(props || {})); + } + function trueFn() { + return true; + } + var propTraps = { + get(_, property, receiver) { + if (property === $PROXY) + return receiver; + return _.get(property); + }, + has(_, property) { + if (property === $PROXY) + return true; + return _.has(property); + }, + set: trueFn, + deleteProperty: trueFn, + getOwnPropertyDescriptor(_, property) { + return { + configurable: true, + enumerable: true, + get() { + return _.get(property); + }, + set: trueFn, + deleteProperty: trueFn + }; + }, + ownKeys(_) { + return _.keys(); + } + }; + function resolveSource(s) { + return !(s = typeof s === "function" ? s() : s) ? {} : s; + } + function resolveSources() { + for (let i = 0, length = this.length;i < length; ++i) { + const v = this[i](); + if (v !== undefined) + return v; + } + } + function mergeProps(...sources) { + let proxy = false; + for (let i = 0;i < sources.length; i++) { + const s = sources[i]; + proxy = proxy || !!s && $PROXY in s; + sources[i] = typeof s === "function" ? (proxy = true, createMemo(s)) : s; + } + if (SUPPORTS_PROXY && proxy) { + return new Proxy({ + get(property) { + for (let i = sources.length - 1;i >= 0; i--) { + const v = resolveSource(sources[i])[property]; + if (v !== undefined) + return v; + } + }, + has(property) { + for (let i = sources.length - 1;i >= 0; i--) { + if (property in resolveSource(sources[i])) + return true; + } + return false; + }, + keys() { + const keys = []; + for (let i = 0;i < sources.length; i++) + keys.push(...Object.keys(resolveSource(sources[i]))); + return [...new Set(keys)]; + } + }, propTraps); + } + const sourcesMap = {}; + const defined = Object.create(null); + for (let i = sources.length - 1;i >= 0; i--) { + const source = sources[i]; + if (!source) + continue; + const sourceKeys = Object.getOwnPropertyNames(source); + for (let i2 = sourceKeys.length - 1;i2 >= 0; i2--) { + const key = sourceKeys[i2]; + if (key === "__proto__" || key === "constructor") + continue; + const desc = Object.getOwnPropertyDescriptor(source, key); + if (!defined[key]) { + defined[key] = desc.get ? { + enumerable: true, + configurable: true, + get: resolveSources.bind(sourcesMap[key] = [desc.get.bind(source)]) + } : desc.value !== undefined ? desc : undefined; + } else { + const sources2 = sourcesMap[key]; + if (sources2) { + if (desc.get) + sources2.push(desc.get.bind(source)); + else if (desc.value !== undefined) + sources2.push(() => desc.value); + } + } + } + } + const target = {}; + const definedKeys = Object.keys(defined); + for (let i = definedKeys.length - 1;i >= 0; i--) { + const key = definedKeys[i], desc = defined[key]; + if (desc && desc.get) + Object.defineProperty(target, key, desc); + else + target[key] = desc ? desc.value : undefined; + } + return target; + } + var narrowedError = (name) => `Stale read from <${name}>.`; + function Show(props) { + const keyed = props.keyed; + const conditionValue = createMemo(() => props.when, undefined, undefined); + const condition = keyed ? conditionValue : createMemo(conditionValue, undefined, { + equals: (a, b) => !a === !b + }); + return createMemo(() => { + const c = condition(); + if (c) { + const child = props.children; + const fn = typeof child === "function" && child.length > 0; + return fn ? untrack(() => child(keyed ? c : () => { + if (!untrack(condition)) + throw narrowedError("Show"); + return conditionValue(); + })) : child; + } + return props.fallback; + }, undefined, undefined); + } + + // node_modules/solid-js/universal/dist/universal.js + var memo = (fn) => createMemo(() => fn()); + function createRenderer$1({ + createElement, + createTextNode, + isTextNode, + replaceText, + insertNode, + removeNode, + setProperty, + getParentNode, + getFirstChild, + getNextSibling + }) { + function insert(parent, accessor, marker, initial) { + if (marker !== undefined && !initial) + initial = []; + if (typeof accessor !== "function") + return insertExpression(parent, accessor, initial, marker); + createRenderEffect((current) => insertExpression(parent, accessor(), current, marker), initial); + } + function insertExpression(parent, value, current, marker, unwrapArray) { + while (typeof current === "function") + current = current(); + if (value === current) + return current; + const t = typeof value, multi = marker !== undefined; + if (t === "string" || t === "number") { + if (t === "number") + value = value.toString(); + if (multi) { + let node = current[0]; + if (node && isTextNode(node)) { + replaceText(node, value); + } else + node = createTextNode(value); + current = cleanChildren(parent, current, marker, node); + } else { + if (current !== "" && typeof current === "string") { + replaceText(getFirstChild(parent), current = value); + } else { + cleanChildren(parent, current, marker, createTextNode(value)); + current = value; + } + } + } else if (value == null || t === "boolean") { + current = cleanChildren(parent, current, marker); + } else if (t === "function") { + createRenderEffect(() => { + let v = value(); + while (typeof v === "function") + v = v(); + current = insertExpression(parent, v, current, marker); + }); + return () => current; + } else if (Array.isArray(value)) { + const array = []; + if (normalizeIncomingArray(array, value, unwrapArray)) { + createRenderEffect(() => current = insertExpression(parent, array, current, marker, true)); + return () => current; + } + if (array.length === 0) { + const replacement = cleanChildren(parent, current, marker); + if (multi) + return current = replacement; + } else { + if (Array.isArray(current)) { + if (current.length === 0) { + appendNodes(parent, array, marker); + } else + reconcileArrays(parent, current, array); + } else if (current == null || current === "") { + appendNodes(parent, array); + } else { + reconcileArrays(parent, multi && current || [getFirstChild(parent)], array); + } + } + current = array; + } else { + if (Array.isArray(current)) { + if (multi) + return current = cleanChildren(parent, current, marker, value); + cleanChildren(parent, current, null, value); + } else if (current == null || current === "" || !getFirstChild(parent)) { + insertNode(parent, value); + } else + replaceNode(parent, value, getFirstChild(parent)); + current = value; + } + return current; + } + function normalizeIncomingArray(normalized, array, unwrap) { + let dynamic = false; + for (let i = 0, len = array.length;i < len; i++) { + let item = array[i], t; + if (item == null || item === true || item === false) + ; + else if (Array.isArray(item)) { + dynamic = normalizeIncomingArray(normalized, item) || dynamic; + } else if ((t = typeof item) === "string" || t === "number") { + normalized.push(createTextNode(item)); + } else if (t === "function") { + if (unwrap) { + while (typeof item === "function") + item = item(); + dynamic = normalizeIncomingArray(normalized, Array.isArray(item) ? item : [item]) || dynamic; + } else { + normalized.push(item); + dynamic = true; + } + } else + normalized.push(item); + } + return dynamic; + } + function reconcileArrays(parentNode, a, b) { + let bLength = b.length, aEnd = a.length, bEnd = bLength, aStart = 0, bStart = 0, after = getNextSibling(a[aEnd - 1]), map = null; + while (aStart < aEnd || bStart < bEnd) { + if (a[aStart] === b[bStart]) { + aStart++; + bStart++; + continue; + } + while (a[aEnd - 1] === b[bEnd - 1]) { + aEnd--; + bEnd--; + } + if (aEnd === aStart) { + const node = bEnd < bLength ? bStart ? getNextSibling(b[bStart - 1]) : b[bEnd - bStart] : after; + while (bStart < bEnd) + insertNode(parentNode, b[bStart++], node); + } else if (bEnd === bStart) { + while (aStart < aEnd) { + if (!map || !map.has(a[aStart])) + removeNode(parentNode, a[aStart]); + aStart++; + } + } else if (a[aStart] === b[bEnd - 1] && b[bStart] === a[aEnd - 1]) { + const node = getNextSibling(a[--aEnd]); + insertNode(parentNode, b[bStart++], getNextSibling(a[aStart++])); + insertNode(parentNode, b[--bEnd], node); + a[aEnd] = b[bEnd]; + } else { + if (!map) { + map = new Map; + let i = bStart; + while (i < bEnd) + map.set(b[i], i++); + } + const index = map.get(a[aStart]); + if (index != null) { + if (bStart < index && index < bEnd) { + let i = aStart, sequence = 1, t; + while (++i < aEnd && i < bEnd) { + if ((t = map.get(a[i])) == null || t !== index + sequence) + break; + sequence++; + } + if (sequence > index - bStart) { + const node = a[aStart]; + while (bStart < index) + insertNode(parentNode, b[bStart++], node); + } else + replaceNode(parentNode, b[bStart++], a[aStart++]); + } else + aStart++; + } else + removeNode(parentNode, a[aStart++]); + } + } + } + function cleanChildren(parent, current, marker, replacement) { + if (marker === undefined) { + let removed; + while (removed = getFirstChild(parent)) + removeNode(parent, removed); + replacement && insertNode(parent, replacement); + return ""; + } + const node = replacement || createTextNode(""); + if (current.length) { + let inserted = false; + for (let i = current.length - 1;i >= 0; i--) { + const el = current[i]; + if (node !== el) { + const isParent = getParentNode(el) === parent; + if (!inserted && !i) + isParent ? replaceNode(parent, node, el) : insertNode(parent, node, marker); + else + isParent && removeNode(parent, el); + } else + inserted = true; + } + } else + insertNode(parent, node, marker); + return [node]; + } + function appendNodes(parent, array, marker) { + for (let i = 0, len = array.length;i < len; i++) + insertNode(parent, array[i], marker); + } + function replaceNode(parent, newNode, oldNode) { + insertNode(parent, newNode, oldNode); + removeNode(parent, oldNode); + } + function spreadExpression(node, props, prevProps = {}, skipChildren) { + props || (props = {}); + if (!skipChildren) { + createRenderEffect(() => prevProps.children = insertExpression(node, props.children, prevProps.children)); + } + createRenderEffect(() => props.ref && props.ref(node)); + createRenderEffect(() => { + for (const prop in props) { + if (prop === "children" || prop === "ref") + continue; + const value = props[prop]; + if (value === prevProps[prop]) + continue; + setProperty(node, prop, value, prevProps[prop]); + prevProps[prop] = value; + } + }); + return prevProps; + } + return { + render(code, element) { + let disposer; + createRoot((dispose) => { + disposer = dispose; + insert(element, code()); + }); + return disposer; + }, + insert, + spread(node, accessor, skipChildren) { + if (typeof accessor === "function") { + createRenderEffect((current) => spreadExpression(node, accessor(), current, skipChildren)); + } else + spreadExpression(node, accessor, undefined, skipChildren); + }, + createElement, + createTextNode, + insertNode, + setProp(node, name, value, prev) { + setProperty(node, name, value, prev); + return value; + }, + mergeProps, + effect: createRenderEffect, + memo, + createComponent, + use(fn, element, arg) { + return untrack(() => fn(element, arg)); + } + }; + } + function createRenderer(options) { + const renderer = createRenderer$1(options); + renderer.mergeProps = mergeProps; + return renderer; + } + + // contracts/spec/spec.ts + var SCREEN_W = 480; + var SCREEN_H = 272; + var NODE_TYPE = { + view: 0, + text: 1, + image: 2 + }; + var ROOT_ID = 1; + var STYLE_ID_NONE = -1; + var PROP = { + width: 1, + height: 2, + minW: 3, + minH: 4, + maxW: 5, + maxH: 6, + paddingT: 8, + paddingR: 9, + paddingB: 10, + paddingL: 11, + marginT: 12, + marginR: 13, + marginB: 14, + marginL: 15, + gap: 16, + flexDir: 17, + justify: 18, + align: 19, + grow: 20, + shrink: 21, + basis: 22, + flexWrap: 23, + posType: 24, + insetT: 25, + insetR: 26, + insetB: 27, + insetL: 28, + display: 29, + overflow: 30, + zIndex: 31, + bgColor: 64, + gradFrom: 65, + gradTo: 66, + gradDir: 67, + radius: 68, + opacity: 69, + borderColor: 70, + borderWidth: 71, + shadow: 72, + bevelOuterLight: 77, + bevelOuterDark: 78, + bevelInnerLight: 79, + bevelInnerDark: 80, + bevelWidth: 81, + textColor: 96, + fontSlot: 97, + textAlign: 98, + lineHeight: 99, + tracking: 100, + translateX: 128, + translateY: 129, + scale: 130, + rotate: 131, + scaleX: 132, + scaleY: 133, + originX: 134, + originY: 135, + rotateX: 136, + rotateY: 137, + translateZ: 138, + perspective: 139, + arcStart: 140, + arcSweep: 141, + arcWidth: 142 + }; + var ANIMATABLE = [ + "width", + "height", + "paddingT", + "paddingR", + "paddingB", + "paddingL", + "marginT", + "marginR", + "marginB", + "marginL", + "gap", + "basis", + "insetT", + "insetR", + "insetB", + "insetL", + "bgColor", + "gradFrom", + "gradTo", + "radius", + "opacity", + "borderColor", + "borderWidth", + "textColor", + "lineHeight", + "tracking", + "translateX", + "translateY", + "scale", + "rotate", + "scaleX", + "scaleY", + "rotateX", + "rotateY", + "translateZ", + "arcStart", + "arcSweep", + "arcWidth" + ]; + function animBit(prop) { + return ANIMATABLE.indexOf(prop); + } + var VALUE_KIND = { + f32: 0, + color: 1, + int: 2 + }; + var PROP_VALUE_KIND = { + width: VALUE_KIND.f32, + height: VALUE_KIND.f32, + minW: VALUE_KIND.f32, + minH: VALUE_KIND.f32, + maxW: VALUE_KIND.f32, + maxH: VALUE_KIND.f32, + paddingT: VALUE_KIND.f32, + paddingR: VALUE_KIND.f32, + paddingB: VALUE_KIND.f32, + paddingL: VALUE_KIND.f32, + marginT: VALUE_KIND.f32, + marginR: VALUE_KIND.f32, + marginB: VALUE_KIND.f32, + marginL: VALUE_KIND.f32, + gap: VALUE_KIND.f32, + flexDir: VALUE_KIND.int, + justify: VALUE_KIND.int, + align: VALUE_KIND.int, + grow: VALUE_KIND.f32, + shrink: VALUE_KIND.f32, + basis: VALUE_KIND.f32, + flexWrap: VALUE_KIND.int, + posType: VALUE_KIND.int, + insetT: VALUE_KIND.f32, + insetR: VALUE_KIND.f32, + insetB: VALUE_KIND.f32, + insetL: VALUE_KIND.f32, + display: VALUE_KIND.int, + overflow: VALUE_KIND.int, + zIndex: VALUE_KIND.int, + bgColor: VALUE_KIND.color, + gradFrom: VALUE_KIND.color, + gradTo: VALUE_KIND.color, + gradDir: VALUE_KIND.int, + radius: VALUE_KIND.f32, + opacity: VALUE_KIND.f32, + borderColor: VALUE_KIND.color, + borderWidth: VALUE_KIND.f32, + shadow: VALUE_KIND.int, + bevelOuterLight: VALUE_KIND.color, + bevelOuterDark: VALUE_KIND.color, + bevelInnerLight: VALUE_KIND.color, + bevelInnerDark: VALUE_KIND.color, + bevelWidth: VALUE_KIND.f32, + textColor: VALUE_KIND.color, + fontSlot: VALUE_KIND.int, + textAlign: VALUE_KIND.int, + lineHeight: VALUE_KIND.f32, + tracking: VALUE_KIND.f32, + translateX: VALUE_KIND.f32, + translateY: VALUE_KIND.f32, + scale: VALUE_KIND.f32, + rotate: VALUE_KIND.f32, + scaleX: VALUE_KIND.f32, + scaleY: VALUE_KIND.f32, + originX: VALUE_KIND.f32, + originY: VALUE_KIND.f32, + rotateX: VALUE_KIND.f32, + rotateY: VALUE_KIND.f32, + translateZ: VALUE_KIND.f32, + perspective: VALUE_KIND.f32, + arcStart: VALUE_KIND.f32, + arcSweep: VALUE_KIND.f32, + arcWidth: VALUE_KIND.f32 + }; + var ENUMS = { + FlexDir: { + Row: 0, + Col: 1 + }, + Justify: { + Start: 0, + Center: 1, + End: 2, + Between: 3, + Around: 4 + }, + Align: { + Start: 0, + Center: 1, + End: 2, + Stretch: 3 + }, + PosType: { + Relative: 0, + Absolute: 1 + }, + Display: { + Flex: 0, + None: 1 + }, + Overflow: { + Visible: 0, + Hidden: 1 + }, + TextAlign: { + Left: 0, + Center: 1, + Right: 2 + }, + GradDir: { + ToTop: 0, + ToBottom: 1, + ToLeft: 2, + ToRight: 3 + }, + Easing: { + Linear: 0, + EaseIn: 1, + EaseOut: 2, + EaseInOut: 3, + OutBack: 4, + Spring: 5, + SpringBouncy: 6, + CubicBezier: 7 + } + }; + var PSM = { + PSM_5650: 0, + PSM_4444: 2, + PSM_8888: 3, + PSM_T8: 5 + }; + var IMG_FLAG_RLE = 1 << 0; + var IMG_FLAG_LINEAR = 1 << 1; + var TILESET_FLAG_RLE = 1 << 0; + var TILESET_FLAG_LINEAR = 1 << 1; + var SVC_IMG_MAX_BYTES = 128 * 1024; + var STREAM_FLAG_ENDED = 1 << 0; + var STYLE_VARIANT_BASE = 1 << 0; + var STYLE_VARIANT_FOCUS = 1 << 1; + var STYLE_VARIANT_ACTIVE = 1 << 2; + var STYLE_HAS_TRANSITION = 1 << 3; + var STYLE_HAS_ANIMATION = 1 << 4; + var ANIM_FILL_BACKWARDS = 1 << 0; + var ANIM_FILL_FORWARDS = 1 << 1; + function abgr(r, g, b, a = 255) { + return ((a & 255) << 24 | (b & 255) << 16 | (g & 255) << 8 | r & 255) >>> 0; + } + var FONT_FLAG_BOLD = 1 << 0; + var PAK_MAGIC = 1263551300; + var PAK_VERSION = 1; + var PAK_HEADER_SIZE = 32; + var PAK_ENTRY_SIZE = 24; + var BTN = { + SELECT: 1, + START: 8, + UP: 16, + RIGHT: 32, + DOWN: 64, + LEFT: 128, + LTRIGGER: 256, + RTRIGGER: 512, + TRIANGLE: 4096, + CIRCLE: 8192, + CROSS: 16384, + SQUARE: 32768 + }; + var ANALOG_CENTER = 32896; + var FIXED_DT = 1 / 60; + + // framework/src/host.ts + function hostViewport(ops) { + return ops.__viewport ?? null; + } + var current = null; + function embeddedBuildHostContract() { + const target = ""; + const hostAbi = 0; + return target && hostAbi > 0 ? { + target, + hostAbi + } : null; + } + function assertNativeHostContract(ops, expected = embeddedBuildHostContract()) { + if (!expected) + return; + if (typeof ops.__host !== "string") { + throw new Error(`PocketJS: this bundle targets "${expected.target}" but the native host predates platform ` + "contracts — add __host/__hostAbi to its ui namespace (see framework/src/host.ts HostOps)"); + } + if (ops.__host !== expected.target) { + throw new Error(`PocketJS: native target mismatch (bundle=${expected.target}, host=${ops.__host})`); + } + if (ops.__hostAbi !== expected.hostAbi) { + throw new Error(`PocketJS: native host ABI mismatch (bundle=${expected.hostAbi}, host=${ops.__hostAbi ?? "missing"})`); + } + } + function detectHost(injected) { + const native = globalThis.ui; + const nativeMarked = native !== undefined && (typeof native.__host === "string" || native.__textures !== undefined); + if (injected) { + if (native !== undefined && injected === native && nativeMarked) { + assertNativeHostContract(native); + return { + ops: injected, + kind: "native", + target: native.__host ?? "unknown", + strict: false + }; + } + return { + ops: injected, + kind: "injected", + target: injected.__host ?? "injected", + strict: true + }; + } + if (native !== undefined && nativeMarked) { + assertNativeHostContract(native); + return { + ops: native, + kind: "native", + target: native.__host ?? "unknown", + strict: false + }; + } + if (native) { + return { + ops: native, + kind: "injected", + target: "injected", + strict: true + }; + } + throw new Error("PocketJS: no host — pass HostOps to render() (web/test) or run under a native runtime (globalThis.ui)"); + } + function installHost(host) { + current = host; + } + function getHost() { + if (!current) { + throw new Error("PocketJS: host not installed — call render() first"); + } + return current; + } + function getOps() { + return getHost().ops; + } + function installFrameHandler(fn) { + globalThis.frame = fn; + } + function installResizeViewportHook(resizeViewport) { + const globals = globalThis; + const previous = globals.__pocketResizeViewport; + const hook = (width, height) => resizeViewport(width, height); + globals.__pocketResizeViewport = hook; + return () => { + if (globals.__pocketResizeViewport !== hook) + return; + if (previous) + globals.__pocketResizeViewport = previous; + else + delete globals.__pocketResizeViewport; + }; + } + function parseHexColor(s) { + let hex = s.slice(1); + if (hex.length === 3) { + hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; + } + if (hex.length !== 6 && hex.length !== 8) { + throw new Error(`PocketJS: bad color '${s}' (expected #rgb/#rrggbb/#rrggbbaa)`); + } + if (!/^[0-9a-fA-F]+$/.test(hex)) + throw new Error(`PocketJS: bad color '${s}'`); + const n = parseInt(hex, 16); + if (hex.length === 6) { + return abgr(n >>> 16 & 255, n >>> 8 & 255, n & 255, 255); + } + return abgr(n >>> 24 & 255, n >>> 16 & 255, n >>> 8 & 255, n & 255); + } + function encodePropValue(prop, value) { + const kind = PROP_VALUE_KIND[prop]; + if (typeof value === "string") { + if (kind === VALUE_KIND.color) + return parseHexColor(value); + const n = Number(value); + if (Number.isNaN(n)) { + throw new Error(`PocketJS: non-numeric value '${value}' for prop '${prop}'`); + } + value = n; + } + if (kind === VALUE_KIND.color || kind === VALUE_KIND.int) + return value >>> 0; + return value; + } + + // framework/src/clock.ts + var TICKS_PER_SECOND = 60; + var VALID_HZ = [1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60]; + var hz = TICKS_PER_SECOND; + var frame = -1; + var timerSeq = 0; + var timers = []; + function normalizeHz(raw) { + if (!Number.isFinite(raw) || raw <= 0) + return TICKS_PER_SECOND; + let best = VALID_HZ[0]; + for (const v of VALID_HZ) { + if (Math.abs(v - raw) < Math.abs(best - raw)) + best = v; + } + return best; + } + function ticksPerFrame() { + return TICKS_PER_SECOND / hz; + } + function virtualFrame() { + return frame < 0 ? 0 : frame; + } + function resetClock() { + const raw = globalThis.__simHz; + hz = typeof raw === "number" ? normalizeHz(raw) : TICKS_PER_SECOND; + frame = -1; + timers = []; + timerSeq = 0; + } + function __advanceClock() { + frame = frame < 0 ? 0 : frame + 1; + if (timers.length === 0) + return; + const due = timers.filter((t) => t.at <= frame).sort((a, b) => a.at - b.at || a.seq - b.seq); + if (due.length === 0) + return; + timers = timers.filter((t) => t.at > frame); + for (const t of due) + t.cb(); + } + + // framework/src/analog.ts + var ANALOG_DEADZONE = 0.12; + var analogPacked = ANALOG_CENTER; + function __setAnalog(packed) { + analogPacked = packed === undefined ? ANALOG_CENTER : packed & 65535; + } + function __resetAnalog() { + analogPacked = ANALOG_CENTER; + } + function axis(raw) { + const value = Math.max(-1, Math.min(1, (raw - 128) / 127)); + const magnitude = Math.abs(value); + if (magnitude < ANALOG_DEADZONE) + return 0; + return Math.sign(value) * (magnitude - ANALOG_DEADZONE) / (1 - ANALOG_DEADZONE); + } + function analogX() { + return axis(analogPacked >> 8 & 255); + } + function analogY() { + return axis(analogPacked & 255); + } + + // framework/src/frame.ts + var callbacks = new Set; + var buttonHandlerBlockDepth = 0; + function resetFrameHooks() { + callbacks.clear(); + buttonHandlerBlockDepth = 0; + __resetAnalog(); + } + function runFrameHooks(buttons) { + for (const cb of [...callbacks]) + cb(buttons); + } + + // framework/src/pak.ts + var map = null; + var bytes = null; + function readKey(u8, off, len) { + let s = ""; + for (let i = 0;i < len; i++) + s += String.fromCharCode(u8[off + i]); + return s; + } + function parse(ab) { + const dv = new DataView(ab); + if (ab.byteLength < PAK_HEADER_SIZE || dv.getUint32(0, true) !== PAK_MAGIC) { + throw new Error("pak: bad magic"); + } + const version = dv.getUint16(4, true); + if (version !== PAK_VERSION) { + throw new Error("pak: unsupported version " + version); + } + const entryCount = dv.getUint32(8, true); + const dirOff = dv.getUint32(12, true); + const namesOff = dv.getUint32(16, true); + const u8 = new Uint8Array(ab); + const m = new Map; + for (let i = 0;i < entryCount; i++) { + const e = dirOff + i * PAK_ENTRY_SIZE; + const blobOff = dv.getUint32(e + 4, true); + const byteLen = dv.getUint32(e + 8, true); + const nameOff = dv.getUint32(e + 12, true); + const nameLen = dv.getUint16(e + 16, true); + const dtype = u8[e + 18]; + m.set(readKey(u8, namesOff + nameOff, nameLen), { + off: blobOff, + len: byteLen, + dtype + }); + } + map = m; + bytes = u8; + } + function loadPack(ab) { + parse(ab); + } + function ensureLoaded() { + if (map) + return; + const ab = globalThis.__pak; + if (!ab) + return; + parse(ab); + } + function hasPack() { + ensureLoaded(); + return map !== null; + } + function entries(prefix = "") { + ensureLoaded(); + if (!map) + return []; + const out = []; + for (const key of map.keys()) { + if (key.length >= prefix.length && key.slice(0, prefix.length) === prefix) { + out.push(key); + } + } + out.sort(); + return out; + } + function get(key) { + ensureLoaded(); + const e = map ? map.get(key) : undefined; + if (!e) { + throw new Error("pak: missing key " + key + " (no __pak provided, or the pack is incomplete)"); + } + return bytes.slice(e.off, e.off + e.len); + } + + // framework/src/input.ts + var root = null; + var focused = null; + var pressedNode = null; + var prevButtons = 0; + var focusScopeStack = []; + var focusGridStack = []; + var focusControllerStack = []; + function setInputRoot(r) { + root = r; + focused = null; + pressedNode = null; + prevButtons = 0; + focusScopeStack.length = 0; + focusGridStack.length = 0; + focusControllerStack.length = 0; + if (cursor) { + cursor.pressTarget = null; + cursor.target = null; + cursor.spriteDirty = true; + cursor.fresh = true; + cursor.vw = 0; + if (cursor.tex >= 0) { + const ops = getOps(); + ops.setCursor?.(-1, 0, 0, 0, 0); + ops.freeTexture?.(cursor.tex); + cursor.tex = -1; + } + } + } + function registerPress(node, fn) { + node.onPress = fn ?? undefined; + } + function registerFocusable(node, on) { + node.focusable = on; + __notifyTreeMutation(); + if (!on && focused === node) { + focusNode(null); + } + } + function focusNode(node) { + if (pressedNode && pressedNode !== node) + setPressedNode(null); + focused = node; + getOps().setFocus(node ? node.id : 0); + } + function setPressedNode(node) { + if (pressedNode === node) + return; + const ops = getOps(); + if (pressedNode) + ops.setActive?.(pressedNode.id, 0); + pressedNode = node; + if (node) + ops.setActive?.(node.id, 1); + } + function activeFocusRoot() { + return focusScopeStack.length > 0 ? focusScopeStack[focusScopeStack.length - 1] : root; + } + function collectFocusables(node, out) { + if (!node) + return; + if (node.focusable) + out.push(node); + if (!Array.isArray(node.children)) + return; + for (let i = 0;i < node.children.length; i++) { + collectFocusables(node.children[i], out); + } + } + function focusables() { + const out = []; + const r = activeFocusRoot(); + if (r) + collectFocusables(r, out); + return out; + } + function linearDirection(direction) { + return direction === "down" || direction === "right" ? 1 : -1; + } + function moveLinearFocus(direction) { + const dir = linearDirection(direction); + const list = focusables(); + if (list.length === 0) { + if (focused) + focusNode(null); + return; + } + const i = focused ? list.indexOf(focused) : -1; + if (i < 0) { + focusNode(dir === 1 ? list[0] : list[list.length - 1]); + return; + } + const j = i + dir; + if (j < 0 || j >= list.length) + return; + focusNode(list[j]); + } + function activeGrid() { + if (!focused) + return null; + const active = activeFocusRoot(); + if (active && !isWithin(focused, active)) + return null; + for (let i = focusGridStack.length - 1;i >= 0; i--) { + const grid = focusGridStack[i]; + if (active && !isWithin(grid.node, active) && !isWithin(active, grid.node)) + continue; + if (isWithin(focused, grid.node)) + return grid; + } + return null; + } + function moveGridFocus(direction) { + const grid = activeGrid(); + if (!grid) + return false; + const list = []; + collectFocusables(grid.node, list); + if (list.length === 0) { + if (focused) + focusNode(null); + return true; + } + const columns = grid.columns; + const i = focused ? list.indexOf(focused) : -1; + if (i < 0) { + focusNode(linearDirection(direction) === 1 ? list[0] : list[list.length - 1]); + return true; + } + let j = i; + switch (direction) { + case "right": + if (i + 1 < list.length && i % columns < columns - 1) + j = i + 1; + else if (grid.wrap) + j = Math.floor(i / columns) * columns; + break; + case "left": + if (i % columns > 0) + j = i - 1; + else if (grid.wrap) + j = Math.min(list.length - 1, Math.floor(i / columns) * columns + columns - 1); + break; + case "down": + if (i + columns < list.length) + j = i + columns; + else if (grid.wrap) + j = i % columns; + break; + case "up": + if (i - columns >= 0) + j = i - columns; + else if (grid.wrap) { + const col = i % columns; + j = col; + while (j + columns < list.length) + j += columns; + } + break; + } + if (j !== i) + focusNode(list[j]); + return true; + } + function activeController() { + if (!focused) + return null; + const active = activeFocusRoot(); + if (active && !isWithin(focused, active)) + return null; + for (let i = focusControllerStack.length - 1;i >= 0; i--) { + const ctl = focusControllerStack[i]; + if (active && !isWithin(ctl.node, active) && !isWithin(active, ctl.node)) + continue; + if (isWithin(focused, ctl.node)) + return ctl; + } + return null; + } + function moveFocus(direction) { + const ctl = activeController(); + if (ctl && ctl.move(direction)) + return; + if (moveGridFocus(direction)) + return; + moveLinearFocus(direction); + } + function firePress() { + let n = focused; + while (n) { + if (n.onPress) { + n.onPress(); + return; + } + n = n.parent; + } + } + function isWithin(node, ancestor) { + if (!node || !ancestor) + return false; + let n = node; + while (n) { + if (n === ancestor) + return true; + n = n.parent; + } + return false; + } + function firstFocusable(node) { + if (!node) + return null; + if (node.focusable) + return node; + if (!Array.isArray(node.children)) + return null; + for (let i = 0;i < node.children.length; i++) { + const f = firstFocusable(node.children[i]); + if (f) + return f; + } + return null; + } + function notifyDetached(node) { + if (!focused || !isWithin(focused, node)) + return; + const parent = node.parent; + if (parent) { + const idx = parent.children.indexOf(node); + for (let i = idx + 1;i < parent.children.length; i++) { + const f = firstFocusable(parent.children[i]); + if (f) { + focusNode(f); + return; + } + } + for (let i = idx - 1;i >= 0; i--) { + const f = firstFocusable(parent.children[i]); + if (f) { + focusNode(f); + return; + } + } + let a = parent; + while (a) { + if (a.focusable) { + focusNode(a); + return; + } + a = a.parent; + } + } + focusNode(null); + } + var cursor = null; + var inputGen = 0; + function __notifyTreeMutation() { + inputGen++; + } + var ARROW_OUTLINE = [1, 3, 5, 9, 17, 33, 65, 129, 257, 513, 1985, 73, 149, 147, 288, 480]; + var ARROW_FILL = [0, 0, 2, 6, 14, 30, 62, 126, 254, 510, 62, 54, 98, 96, 192, 0]; + function defaultArrowRGBA() { + const px = new Uint8Array(16 * 16 * 4); + for (let y = 0;y < 16; y++) { + for (let x = 0;x < 16; x++) { + const outline = ARROW_OUTLINE[y] >> x & 1; + const fill = ARROW_FILL[y] >> x & 1; + if (!outline && !fill) + continue; + const i = (y * 16 + x) * 4; + const v = fill ? 255 : 0; + px[i] = v; + px[i + 1] = v; + px[i + 2] = v; + px[i + 3] = 255; + } + } + return px; + } + function cursorInitSprite(c, ops) { + const sprite = c.sprite; + c.spriteDirty = false; + const old = c.tex; + let tex = -1; + let blob = null; + if (typeof sprite.image === "string") { + try { + blob = get(sprite.image); + } catch (err) { + if (getHost().strict) + throw err; + blob = null; + } + } else if (sprite.image) { + blob = sprite.image; + } + if (blob) { + tex = ops.uploadImgEntry ? ops.uploadImgEntry(blob) : uploadImgFallback(ops, blob); + if (tex < 0 && getHost().strict) { + throw new Error("enableCursor: cursor image rejected (malformed or RLE-only IMG entry)"); + } + } + if (tex < 0) { + tex = ops.uploadTexture(defaultArrowRGBA(), 16, 16, PSM.PSM_8888); + } + c.tex = tex; + ops.setCursor(tex, sprite.hotspot[0], sprite.hotspot[1], sprite.size[0], sprite.size[1]); + if (old >= 0 && old !== tex) + ops.freeTexture?.(old); + } + function uploadImgFallback(ops, blob) { + if (blob.length < 8) + return -1; + const dv = new DataView(blob.buffer, blob.byteOffset, blob.byteLength); + if (blob[5] & IMG_FLAG_RLE) + return -1; + return ops.uploadTexture(blob.subarray(8), dv.getUint16(0, true), dv.getUint16(2, true), blob[4]); + } + function findMirror(node, id) { + if (!node || id === 0) + return null; + if (node.id === id) + return node; + const kids = node.children; + if (!Array.isArray(kids)) + return null; + for (let i = 0;i < kids.length; i++) { + const found = findMirror(kids[i], id); + if (found) + return found; + } + return null; + } + var hitRoot = null; + function setHitRoot(r) { + hitRoot = r; + } + function cursorTarget(hit) { + const scope = focusScopeStack.length > 0 ? focusScopeStack[focusScopeStack.length - 1] : null; + let n = hit; + while (n) { + if (n.focusable && (!scope || isWithin(n, scope))) + return n; + n = n.parent; + } + return null; + } + function cursorFrame(buttons, pressed, released) { + const c = cursor; + const ops = getOps(); + if (!ops.hitTest || !ops.setCursor || !ops.setCursorPos) + return false; + if (c.vw === 0) { + const vp = hostViewport(ops); + c.vw = vp ? vp.w : SCREEN_W; + c.vh = vp ? vp.h : SCREEN_H; + if (c.x < 0) { + c.x = Math.floor(c.vw / 2); + c.y = Math.floor(c.vh / 2); + } + } + if (c.spriteDirty) + cursorInitSprite(c, ops); + let vx = analogX() * c.speed; + let vy = analogY() * c.speed; + if (c.dpadSpeed > 0 && vx === 0 && vy === 0) { + if (buttons & BTN.LEFT) + vx = -c.dpadSpeed; + if (buttons & BTN.RIGHT) + vx = c.dpadSpeed; + if (buttons & BTN.UP) + vy = -c.dpadSpeed; + if (buttons & BTN.DOWN) + vy = c.dpadSpeed; + } + let moved = c.fresh; + if (vx !== 0 || vy !== 0) { + const dt = ticksPerFrame() / 60; + const nx = Math.min(Math.max(c.x + vx * dt, 0), c.vw - 1); + const ny = Math.min(Math.max(c.y + vy * dt, 0), c.vh - 1); + if (nx !== c.x || ny !== c.y) { + c.x = nx; + c.y = ny; + moved = true; + } + } + if (moved) + ops.setCursorPos(c.x, c.y); + const edges = (pressed | released) & c.button; + const gen = inputGen; + if (moved || edges !== 0 || gen !== c.gen) { + c.gen = gen; + c.fresh = false; + c.target = cursorTarget(findMirror(hitRoot ?? root, ops.hitTest(c.x, c.y))); + } + const target = c.target; + if (target !== focused) + focusNode(target); + if (pressed & c.button && target) { + c.pressTarget = target; + } + if (c.pressTarget) { + setPressedNode(target === c.pressTarget ? c.pressTarget : null); + if (released & c.button) { + const fire = target === c.pressTarget; + c.pressTarget = null; + setPressedNode(null); + if (fire) + firePress(); + } + } else if (released & c.button) { + setPressedNode(null); + } + return true; + } + function handleFrame(buttons) { + const pressed = buttons & ~prevButtons; + const released = prevButtons & ~buttons; + prevButtons = buttons; + if (cursor && cursorFrame(buttons, pressed, released)) + return; + if (released & BTN.CIRCLE) + setPressedNode(null); + if (pressed === 0) + return; + if (pressed & BTN.DOWN) + moveFocus("down"); + if (pressed & BTN.RIGHT) + moveFocus("right"); + if (pressed & BTN.UP) + moveFocus("up"); + if (pressed & BTN.LEFT) + moveFocus("left"); + if (pressed & BTN.CIRCLE) { + setPressedNode(focused); + firePress(); + } + } + + // framework/src/native-tree.ts + var treeMutationHook = null; + function setTreeMutationHook(fn) { + treeMutationHook = fn; + } + function treeMutated() { + __notifyTreeMutation(); + if (treeMutationHook) + treeMutationHook(); + } + function setDebugName(node, name) { + node.debugName = name || undefined; + treeMutated(); + } + var rootMirror = { + id: ROOT_ID, + type: NODE_TYPE.view, + parent: null, + children: [], + domNodeType: 1, + domTag: "root" + }; + var DOM_NODE = Symbol.for("pocketjs.native-node"); + var DOM_ELEMENT = 1; + var DOM_TEXT = 3; + var DOM_COMMENT = 8; + var NATIVE_ATTRIBUTE_NAMES = new Set(["class", "className", "style", "src", "onPress", "on:press", "focusable", "debugName", "ref", "nodeRef", "key", "children"]); + function domAttrs(node) { + return node.domAttrs ??= {}; + } + function cloneNativeNode(node, deep) { + const nodeType = node.domNodeType ?? (isTextNode(node) ? DOM_TEXT : DOM_ELEMENT); + const clone = nodeType === DOM_TEXT ? createTextNode(node.text ?? "") : nodeType === DOM_COMMENT ? createCommentNode(node.domData ?? "") : createElement(node.domTag ?? tagName(node)); + for (const key of Object.keys(node.domAttrs ?? {})) { + setDomAttribute(clone, key, node.domAttrs[key]); + } + if (deep) { + for (const child of node.children) + insertNode(clone, cloneNativeNode(child, true)); + } + return clone; + } + function setDomAttribute(node, name, value) { + if (NATIVE_ATTRIBUTE_NAMES.has(name)) { + setProp(node, name, value, node.domAttrs?.[name]); + return; + } + if (value == null) + delete domAttrs(node)[name]; + else + domAttrs(node)[name] = value; + } + function decorateNativeNode(node) { + if (node[DOM_NODE] === true) + return node; + Object.defineProperty(node, DOM_NODE, { + value: true + }); + Object.defineProperties(node, { + nodeType: { + configurable: true, + get() { + return node.domNodeType ?? (isTextNode(node) ? DOM_TEXT : DOM_ELEMENT); + } + }, + nodeValue: { + configurable: true, + get() { + return node.domNodeType === DOM_COMMENT ? node.domData ?? "" : node.text ?? ""; + }, + set(value) { + if (node.domNodeType === DOM_COMMENT) + node.domData = String(value ?? ""); + else + replaceText(node, String(value ?? "")); + } + }, + data: { + configurable: true, + get() { + return node.domNodeType === DOM_COMMENT ? node.domData ?? "" : node.text ?? ""; + }, + set(value) { + if (node.domNodeType === DOM_COMMENT) + node.domData = String(value ?? ""); + else + replaceText(node, String(value ?? "")); + } + }, + textContent: { + configurable: true, + get() { + if (node.domNodeType === DOM_COMMENT) + return node.domData ?? ""; + if (isTextNode(node)) + return node.text ?? ""; + return node.children.map((child) => child.text ?? "").join(""); + }, + set(value) { + const text = String(value ?? ""); + if (node.domNodeType === DOM_COMMENT) { + node.domData = text; + } else if (isTextNode(node)) { + replaceText(node, text); + } else { + clearContainer(node); + if (text) + insertNode(node, createTextNode(text)); + } + } + }, + parentNode: { + configurable: true, + get() { + return node.parent; + } + }, + parentElement: { + configurable: true, + get() { + return node.parent; + } + }, + childNodes: { + configurable: true, + get() { + return node.children; + } + }, + firstChild: { + configurable: true, + get() { + return node.children[0] ?? null; + } + }, + lastChild: { + configurable: true, + get() { + return node.children[node.children.length - 1] ?? null; + } + }, + nextSibling: { + configurable: true, + get() { + return getNextSibling(node) ?? null; + } + }, + previousSibling: { + configurable: true, + get() { + const parent = node.parent; + if (!parent) + return null; + const index = parent.children.indexOf(node); + return index > 0 ? parent.children[index - 1] : null; + } + }, + tagName: { + configurable: true, + get() { + return (node.domTag ?? tagName(node)).toUpperCase(); + } + }, + nodeName: { + configurable: true, + get() { + if (node.domNodeType === DOM_TEXT) + return "#text"; + if (node.domNodeType === DOM_COMMENT) + return "#comment"; + return (node.domTag ?? tagName(node)).toUpperCase(); + } + }, + className: { + configurable: true, + get() { + return String(node.domAttrs?.class ?? ""); + }, + set(value) { + setProp(node, "class", value, node.domAttrs?.class); + } + }, + isConnected: { + configurable: true, + get() { + let current2 = node; + while (current2) { + if (current2 === rootMirror) + return true; + current2 = current2.parent; + } + return false; + } + } + }); + const methods = { + appendChild(child) { + insertNode(node, child); + return child; + }, + insertBefore(child, anchor) { + insertNode(node, child, anchor ?? null); + return child; + }, + removeChild(child) { + removeNode(node, child); + return child; + }, + replaceChild(next, current2) { + insertNode(node, next, current2); + removeNode(node, current2); + return current2; + }, + cloneNode(deep = false) { + return cloneNativeNode(node, !!deep); + }, + remove() { + if (node.parent) + removeNode(node.parent, node); + }, + setAttribute(name, value) { + setDomAttribute(node, name, value); + }, + removeAttribute(name) { + setDomAttribute(node, name, undefined); + }, + getAttribute(name) { + const value = node.domAttrs?.[name]; + return value == null ? null : String(value); + }, + hasAttribute(name) { + return node.domAttrs?.[name] != null; + }, + hasChildNodes() { + return node.children.length > 0; + }, + contains(other) { + let current2 = other ?? null; + while (current2) { + if (current2 === node) + return true; + current2 = current2.parent; + } + return false; + }, + addEventListener() {}, + removeEventListener() {} + }; + Object.assign(node, methods, { + style: { + length: 0, + item: () => "" + }, + classList: { + add() {}, + remove() {} + } + }); + return node; + } + decorateNativeNode(rootMirror); + var styleResolver = null; + function setStyleResolver(fn) { + styleResolver = fn; + } + var missCounters = { + unknownClass: 0, + unknownTexture: 0 + }; + var textures = new Map; + function registerTexture(key, handle) { + textures.set(key, handle); + } + var sprites = new Map; + function registerSprite(key, meta) { + sprites.set(key, meta); + } + var sweepSet = new Set; + var retained = new Set; + function subtreeHasRetained(node) { + if (!node) + return false; + if (retained.has(node)) + return true; + if (node.children) { + for (let i = 0;i < node.children.length; i++) { + if (subtreeHasRetained(node.children[i])) + return true; + } + } + return false; + } + function runSweep() { + if (sweepSet.size === 0) + return; + const ops = getOps(); + const keep = []; + for (const node of sweepSet) { + if (!node) + continue; + if (node.parent !== null) + continue; + if (subtreeHasRetained(node)) { + keep.push(node); + continue; + } + ops.destroyNode(node.id); + } + sweepSet.clear(); + for (let i = 0;i < keep.length; i++) + sweepSet.add(keep[i]); + } + function createElement(tag) { + const type = NODE_TYPE[tag]; + if (type === undefined) { + throw new Error(`PocketJS: unknown element <${tag}> - only view/text/image exist`); + } + return decorateNativeNode({ + id: getOps().createNode(type), + type, + parent: null, + children: [], + domNodeType: DOM_ELEMENT, + domTag: tag + }); + } + function createTextNode(value) { + const ops = getOps(); + const id = ops.createNode(NODE_TYPE.text); + ops.setText(id, value); + return decorateNativeNode({ + id, + type: NODE_TYPE.text, + parent: null, + children: [], + text: value, + domNodeType: DOM_TEXT, + domTag: "#text" + }); + } + function createCommentNode(data = "") { + const node = createTextNode(""); + node.domNodeType = DOM_COMMENT; + node.domTag = "#comment"; + node.domData = data; + return node; + } + function replaceText(node, value) { + getOps().replaceText(node.id, value); + node.text = value; + treeMutated(); + } + function isTextNode(node) { + return node.type === NODE_TYPE.text; + } + function unlink(node) { + const p = node.parent; + if (!p) + return; + const i = p.children.indexOf(node); + if (i >= 0) + p.children.splice(i, 1); + node.parent = null; + } + function insertNode(parent, node, anchor) { + const ops = getOps(); + unlink(node); + sweepSet.delete(node); + ops.insertBefore(parent.id, node.id, anchor ? anchor.id : 0); + if (anchor) { + const i = parent.children.indexOf(anchor); + if (i < 0) + throw new Error("PocketJS: insert anchor is not a child of parent"); + parent.children.splice(i, 0, node); + } else { + parent.children.push(node); + } + node.parent = parent; + treeMutated(); + } + function removeNode(parent, node) { + if (!node) + return; + notifyDetached(node); + getOps().removeChild(parent.id, node.id); + unlink(node); + sweepSet.add(node); + treeMutated(); + } + function getParentNode(node) { + return node.parent ?? undefined; + } + function getFirstChild(node) { + return node.children[0]; + } + function getNextSibling(node) { + const p = node.parent; + if (!p) + return; + const i = p.children.indexOf(node); + return i >= 0 ? p.children[i + 1] : undefined; + } + function setClass(node, value) { + const ops = getOps(); + treeMutated(); + if (value == null || value === "") { + ops.setStyle(node.id, STYLE_ID_NONE); + return; + } + if (typeof value !== "string") { + throw new Error("PocketJS: class must be a string literal of utilities"); + } + const styleId = styleResolver ? styleResolver(value) : undefined; + if (styleId === undefined) { + if (getHost().strict) { + throw new Error(`PocketJS: unknown class "${value}" - not in the compiled style table ` + "(dynamic classes must be ternaries of full literals)"); + } + missCounters.unknownClass++; + return; + } + ops.setStyle(node.id, styleId); + } + function setSrc(node, value) { + const ops = getOps(); + if (value == null || value === "") { + ops.setImage(node.id, -1); + return; + } + if (typeof value !== "string") { + throw new Error("PocketJS: src must be a string key"); + } + const handle = textures.get(value); + if (handle === undefined) { + if (getHost().strict) { + throw new Error(`PocketJS: unknown image src "${value}" - no texture registered under that key`); + } + missCounters.unknownTexture++; + return; + } + ops.setImage(node.id, handle); + } + function setSpriteSrc(node, value) { + const ops = getOps(); + if (value == null || value === "") { + ops.setSprite(node.id, -1, 0, 0, 0); + return; + } + if (typeof value !== "string") { + throw new Error("PocketJS: sprite must be a string key"); + } + const meta = sprites.get(value); + if (meta === undefined) { + if (getHost().strict) { + throw new Error(`PocketJS: unknown sprite "${value}" - no sprite atlas registered under that key`); + } + missCounters.unknownTexture++; + return; + } + ops.setSprite(node.id, meta.handle, meta.frames, meta.cols, meta.step); + } + function setStyleObject(node, value, prev) { + const ops = getOps(); + const next = value ?? {}; + const before = prev ?? {}; + let changed = false; + for (const key in next) { + const v = next[key]; + if (before[key] === v) + continue; + const propId = PROP[key]; + if (propId === undefined) { + throw new Error(`PocketJS: unknown style prop '${key}' (see spec PROP)`); + } + ops.setProp(node.id, propId, encodePropValue(key, v)); + changed = true; + } + if (changed) + treeMutated(); + } + function setProp(node, name, value, prev) { + if (value === prev && name !== "style") + return value; + if (name === "className") + name = "class"; + if (name !== "children" && name !== "key" && name !== "ref" && name !== "nodeRef") { + if (value == null) + delete domAttrs(node)[name]; + else + domAttrs(node)[name] = value; + } + switch (name) { + case "class": + setClass(node, value); + return value; + case "onPress": + case "on:press": + registerPress(node, value); + return value; + case "src": + setSrc(node, value); + return value; + case "sprite": + setSpriteSrc(node, value); + return value; + case "style": + setStyleObject(node, value, prev); + return value; + case "focusable": + registerFocusable(node, !!value); + return value; + case "debugName": + setDebugName(node, value == null ? undefined : String(value)); + return value; + case "ref": + case "nodeRef": + case "key": + case "children": + return value; + default: + break; + } + if (name === "classList") { + throw new Error("PocketJS: classList is not supported - use ternaries of full class literals"); + } + if (name.startsWith("on:") || name.startsWith("bool:") || name.startsWith("prop:")) { + throw new Error(`PocketJS: unsupported namespaced attribute '${name}'`); + } + throw new Error(`PocketJS: unknown property '${name}' on <${tagName(node)}>`); + } + function clearContainer(container) { + for (const child of [...container.children]) + removeNode(container, child); + } + function tagName(node) { + for (const key of Object.keys(NODE_TYPE)) { + if (NODE_TYPE[key] === node.type) + return key; + } + return String(node.type); + } + + // framework/src/renderer-solid.ts + function setProperty(node, name, value, prev) { + if (name === "ref" && typeof value === "function") { + value(node); + return; + } + setProp(node, name, value, prev); + } + var renderer = createRenderer({ + createElement, + createTextNode, + replaceText, + isTextNode, + setProperty, + insertNode(parent, node, anchor) { + insertNode(parent, node, anchor); + }, + removeNode(parent, node) { + removeNode(parent, node); + }, + getParentNode, + getFirstChild, + getNextSibling + }); + var { + render, + effect, + memo: memo2, + createComponent: createComponent2, + createElement: createElement2, + insert, + spread, + mergeProps: mergeProps2, + use + } = renderer; + + // framework/src/anim.ts + var EASING_BY_NAME = { + linear: ENUMS.Easing.Linear, + in: ENUMS.Easing.EaseIn, + out: ENUMS.Easing.EaseOut, + "in-out": ENUMS.Easing.EaseInOut, + "out-back": ENUMS.Easing.OutBack, + spring: ENUMS.Easing.Spring, + "spring-bouncy": ENUMS.Easing.SpringBouncy + }; + function nodeId(node) { + return typeof node === "number" ? node : node.id; + } + function animatablePropId(prop) { + const propId = PROP[prop]; + if (propId === undefined) { + throw new Error(`PocketJS: unknown prop '${prop}'`); + } + if (animBit(prop) < 0) { + throw new Error(`PocketJS: prop '${prop}' is not animatable (see spec ANIMATABLE)`); + } + return propId; + } + function animate(node, prop, to, opts = {}) { + const propId = animatablePropId(prop); + let easing; + if (typeof opts.easing === "number") { + easing = opts.easing; + } else { + const named = EASING_BY_NAME[opts.easing ?? "out"]; + if (named === undefined) { + throw new Error(`PocketJS: unknown easing '${opts.easing}'`); + } + easing = named; + } + return getOps().animate(nodeId(node), propId, encodePropValue(prop, to), opts.dur ?? 200, easing, opts.delay ?? 0); + } + function spring(node, prop, to, preset = "default") { + const propId = animatablePropId(prop); + const easing = preset === "bouncy" ? ENUMS.Easing.SpringBouncy : ENUMS.Easing.Spring; + return getOps().animate(nodeId(node), propId, encodePropValue(prop, to), 0, easing, 0); + } + + // framework/src/overlay.ts + var overlayRoot = null; + function setOverlayRoot(root2) { + overlayRoot = root2; + } + // framework/src/primitives.ts + function callRef(ref, node) { + if (!ref) + return; + if (typeof ref === "function") + ref(node); + else if ("current" in ref) + ref.current = node; + } + function primitive(tag, props) { + const el = createElement2(tag); + spread(el, props, false); + callRef(props.nodeRef, el); + return el; + } + function View(props) { + return primitive("view", props); + } + function Text(props) { + return primitive("text", props); + } + // framework/src/hot.ts + var lastText = new WeakMap; + var lastProp = new WeakMap; + + // framework/src/platform.ts + var features = {} !== null ? Object.freeze({ + ...{} + }) : Object.freeze({}); + var platform = Object.freeze({ + target: "", + pixelRatio: Number.isInteger(1) ? 1 : 1, + features + }); + + // framework/src/tiles.ts + var parsed = new Map; + // apps/cards/app.tsx + var CARDS = [{ + title: "Layout", + caption: "Flexbox via Taffy", + detail: "Rows, columns, gaps and insets — solved natively in Rust.", + cls: "flex-col gap-1 p-3 w-[136] rounded-xl shadow-md overflow-hidden bg-white border-slate-200 translate-y-1 focus:bg-blue-50 focus:border-blue-500 focus:translate-y-0 transition-all duration-150 ease-out", + strip: "h-1 w-full rounded-sm bg-gradient-to-r from-blue-500 to-blue-600", + bar: "w-1 h-7 bg-blue-500" + }, { + title: "Motion", + caption: "Springs and tweens", + detail: "Fixed-dt springs and tweens tick natively at 60 FPS.", + cls: "flex-col gap-1 p-3 w-[136] rounded-xl shadow-md overflow-hidden bg-white border-slate-200 translate-y-1 focus:bg-emerald-50 focus:border-emerald-500 focus:translate-y-0 transition-all duration-150 ease-out", + strip: "h-1 w-full rounded-sm bg-gradient-to-r from-emerald-500 to-emerald-600", + bar: "w-1 h-7 bg-emerald-500" + }, { + title: "Input", + caption: "D-pad and focus", + detail: "Native focus variants respond before JS even wakes up.", + cls: "flex-col gap-1 p-3 w-[136] rounded-xl shadow-md overflow-hidden bg-white border-slate-200 translate-y-1 focus:bg-amber-50 focus:border-amber-500 focus:translate-y-0 transition-all duration-150 ease-out", + strip: "h-1 w-full rounded-sm bg-gradient-to-r from-amber-500 to-amber-600", + bar: "w-1 h-7 bg-amber-500" + }]; + function Detail(props) { + let el; + onMount(() => { + if (el) + spring(el, "translateY", 0); + }); + return createComponent2(View, { + ref(r$) { + var _ref$ = el; + typeof _ref$ === "function" ? _ref$(r$) : el = r$; + }, + debugName: "Detail", + style: { + translateY: 22 + }, + class: "flex-row items-center gap-3 p-3 rounded-xl shadow-md bg-white border-slate-200", + get children() { + return [createComponent2(View, { + get ["class"]() { + return props.card.bar; + } + }), createComponent2(View, { + class: "flex-col gap-1", + get children() { + return [createComponent2(Text, { + class: "text-sm text-slate-950 font-bold", + get children() { + return props.card.title; + } + }), createComponent2(Text, { + class: "text-xs text-slate-600", + get children() { + return props.card.detail; + } + })]; + } + })]; + } + }); + } + function Cards() { + const [open, setOpen] = createSignal(-1); + const selected = () => open() >= 0 ? CARDS[open()] : undefined; + let streakA; + let streakB; + onMount(() => { + if (streakA) + animate(streakA, "translateX", 300, { + dur: 20000, + easing: "linear" + }); + if (streakB) + animate(streakB, "translateX", -260, { + dur: 26000, + easing: "linear" + }); + }); + return createComponent2(View, { + debugName: "CardsScreen", + class: "relative flex-col w-full h-full p-4 gap-3 bg-slate-50 overflow-hidden", + get children() { + return [createComponent2(View, { + ref(r$) { + var _ref$2 = streakA; + typeof _ref$2 === "function" ? _ref$2(r$) : streakA = r$; + }, + class: "absolute left-0 top-[58] w-64 h-1 rounded-full opacity-50 bg-gradient-to-r from-blue-300 to-transparent", + style: { + translateX: 24 + } + }), createComponent2(View, { + ref(r$) { + var _ref$3 = streakB; + typeof _ref$3 === "function" ? _ref$3(r$) : streakB = r$; + }, + class: "absolute left-[210] top-[246] w-56 h-1 rounded-full opacity-40 bg-gradient-to-l from-cyan-300 to-transparent", + style: { + translateX: 0 + } + }), createComponent2(View, { + debugName: "Header", + class: "flex-row items-end justify-between", + get children() { + return [createComponent2(View, { + class: "flex-col", + get children() { + return [createComponent2(Text, { + class: "text-xs text-blue-600 tracking-wide", + children: "POCKETJS SHOWCASE" + }), createComponent2(Text, { + class: "text-2xl text-slate-950 font-bold", + children: "Feature Cards" + })]; + } + }), createComponent2(Text, { + class: "text-xs text-slate-500", + children: "3 MODULES" + })]; + } + }), createComponent2(View, { + debugName: "CardRow", + class: "flex-row gap-3", + get children() { + return CARDS.map((card, i) => createComponent2(View, { + get ["class"]() { + return card.cls; + }, + focusable: true, + onPress: () => setOpen(open() === i ? -1 : i), + get children() { + return [createComponent2(View, { + get ["class"]() { + return card.strip; + } + }), createComponent2(Text, { + class: "text-sm text-slate-950 font-bold", + get children() { + return card.title; + } + }), createComponent2(Text, { + class: "text-xs text-slate-600", + get children() { + return card.caption; + } + })]; + } + })); + } + }), createComponent2(View, { + debugName: "DetailPane", + class: "grow flex-col", + get children() { + return createComponent2(Show, { + get when() { + return selected(); + }, + keyed: true, + children: (card) => createComponent2(Detail, { + card + }) + }); + } + }), createComponent2(Text, { + class: "text-xs text-slate-500", + children: "LEFT / RIGHT move focus · CIRCLE toggle details" + })]; + } + }); + } + + // framework/src/devtools.ts + var TAPE_CAP = 36000; + var TREE_THROTTLE = 30; + var STATS_EVERY = 30; + var state = { + ops: null, + transport: null, + app: undefined, + frame: 0, + tape: new Uint16Array(TAPE_CAP), + tapeAnalog: new Uint16Array(TAPE_CAP), + tapeStart: 0, + tapeLen: 0, + tapeFirstFrame: 0, + replayMasks: null, + replayAnalog: null, + replayAt: 0, + paused: false, + stepQueued: 0, + inspectReportId: null, + inspectAskedAt: 0, + treeDirty: true, + treeSentAt: -TREE_THROTTLE, + saidHello: false, + hostCalls: 0 + }; + function initDevtools(ops) { + const g = globalThis; + if (!g.console) + g.console = { + log() {}, + warn() {}, + error() {} + }; + state.ops = ops; + state.frame = 0; + state.tapeStart = 0; + state.tapeLen = 0; + state.tapeFirstFrame = 0; + state.replayMasks = null; + state.replayAnalog = null; + state.paused = false; + state.stepQueued = 0; + state.inspectReportId = null; + state.inspectAskedAt = 0; + state.treeDirty = true; + state.treeSentAt = -TREE_THROTTLE; + state.saidHello = false; + state.hostCalls = 0; + state.app = globalThis.__pocketApp; + const injected = globalThis.__pocketDevtoolsTransport; + if (injected) { + state.transport = injected; + } else if (ops.__dbgActive?.() && ops.__dbgPoll && ops.__dbgSend) { + state.transport = { + send: (l) => ops.__dbgSend(l), + recv: () => ops.__dbgPoll(), + everyFrames: 10 + }; + } else { + state.transport = null; + } + if (state.transport) { + setTreeMutationHook(() => { + state.treeDirty = true; + }); + bridgeConsole(); + } else { + setTreeMutationHook(null); + } + globalThis.__pocketDevtools = api; + } + function wrapFrameHandler(h) { + return (buttons, analogArg, touchArg) => { + state.hostCalls++; + if (state.transport) { + pollTransport(); + flushInspectReport(); + } + let mask = buttons; + let analog = analogArg === undefined ? ANALOG_CENTER : analogArg & 65535; + let touch = touchArg; + if (state.replayMasks) { + if (state.replayAt < state.replayMasks.length) { + mask = state.replayMasks[state.replayAt]; + analog = state.replayAnalog ? state.replayAnalog[state.replayAt] : ANALOG_CENTER; + touch = undefined; + state.replayAt++; + } else { + state.replayMasks = null; + state.replayAnalog = null; + send({ + t: "replayDone", + frame: state.frame + }); + } + } + if (state.paused) { + if (state.stepQueued <= 0) + return; + state.stepQueued--; + state.ops?.debugStep?.(); + } + recordMask(mask, analog); + state.frame++; + try { + h(mask, analog, touch); + } catch (e) { + send({ + t: "error", + frame: state.frame, + message: e instanceof Error ? e.message : String(e), + stack: e instanceof Error ? e.stack : undefined + }); + throw e; + } + if (state.transport) + afterFrame(); + }; + } + function recordMask(mask, analog) { + if (state.tapeLen < TAPE_CAP) { + const at = (state.tapeStart + state.tapeLen) % TAPE_CAP; + state.tape[at] = mask; + state.tapeAnalog[at] = analog; + state.tapeLen++; + } else { + state.tape[state.tapeStart] = mask; + state.tapeAnalog[state.tapeStart] = analog; + state.tapeStart = (state.tapeStart + 1) % TAPE_CAP; + state.tapeFirstFrame++; + } + } + function rlePairs(ring) { + const out = []; + for (let i = 0;i < state.tapeLen; i++) { + const v = ring[(state.tapeStart + i) % TAPE_CAP]; + const last = out[out.length - 1]; + if (last && last[0] === v) + last[1]++; + else + out.push([v, 1]); + } + return out; + } + function exportTape() { + const tape = { + v: 1, + app: state.app, + frames: state.tapeLen, + masks: rlePairs(state.tape), + startFrame: state.tapeFirstFrame + }; + const analog = rlePairs(state.tapeAnalog); + if (analog.length > 1 || analog.length === 1 && analog[0][0] !== ANALOG_CENTER) { + tape.analog = analog; + } + return tape; + } + function expandPairs(pairs, fill, total) { + const out = new Uint16Array(total).fill(fill); + let at = 0; + for (const [v, n] of pairs) { + out.fill(v, at, Math.min(at + n, total)); + at += n; + } + return out; + } + function expandTape(tape) { + let total = 0; + for (const [, n] of tape.masks) + total += n; + return expandPairs(tape.masks, 0, total); + } + function expandTapeAnalog(tape) { + let total = 0; + for (const [, n] of tape.masks) + total += n; + return expandPairs(tape.analog ?? [], ANALOG_CENTER, total); + } + function send(msg) { + try { + state.transport?.send(JSON.stringify(msg)); + } catch {} + } + function pollTransport() { + const t = state.transport; + const every = t.everyFrames ?? 1; + if (every > 1 && state.hostCalls % every !== 0) + return; + if (!state.saidHello) { + state.saidHello = true; + send({ + t: "hello", + app: state.app, + host: hostKind(), + frame: state.frame + }); + } + for (let guard = 0;guard < 64; guard++) { + const chunk = t.recv(); + if (!chunk) + break; + for (const line of chunk.split(` +`)) { + if (line.trim()) + handleMessage(line); + } + } + } + function handleMessage(line) { + let msg; + try { + msg = JSON.parse(line); + } catch { + return; + } + const ops = state.ops; + switch (msg.t) { + case "inspect": { + const id = typeof msg.id === "number" ? msg.id : 0; + ops?.debugInspect?.(id); + state.inspectReportId = id || null; + state.inspectAskedAt = state.hostCalls; + if (!id) + send({ + t: "inspect", + id: 0, + rect: null + }); + break; + } + case "pause": + state.paused = true; + state.stepQueued = 0; + ops?.debugPause?.(true); + sendStats(); + break; + case "resume": + state.paused = false; + ops?.debugPause?.(false); + sendStats(); + break; + case "step": + state.stepQueued += typeof msg.n === "number" && msg.n > 0 ? msg.n : 1; + break; + case "getTree": + sendTree(); + break; + case "eval": { + let ok = true; + let value; + try { + value = fmt((0, eval)(String(msg.code))); + } catch (e) { + ok = false; + value = e instanceof Error ? `${e.name}: ${e.message}` : String(e); + } + send({ + t: "evalResult", + id: msg.id, + ok, + value + }); + break; + } + case "dumpTape": + send({ + t: "tape", + tape: exportTape() + }); + break; + case "devStats": { + let data = null; + const raw = ops?.debugStats?.(); + if (raw) { + try { + data = JSON.parse(raw); + } catch { + data = null; + } + } + send({ + t: "devStats", + frame: state.frame, + data + }); + break; + } + case "screenshot": { + if (ops?.__dbgShot?.()) { + send({ + t: "screenshotRaw", + file: "shot.raw", + w: 480, + h: 272, + stride: 512, + frame: state.frame + }); + } else { + send({ + t: "log", + level: "warn", + args: ["screenshot: not supported on this host"] + }); + } + break; + } + case "replay": { + const tape = msg.tape; + if (tape && Array.isArray(tape.masks)) { + state.replayMasks = expandTape(tape); + state.replayAnalog = tape.analog ? expandTapeAnalog(tape) : null; + state.replayAt = 0; + } + break; + } + default: + break; + } + } + function afterFrame() { + if (state.treeDirty && state.frame - state.treeSentAt >= TREE_THROTTLE) { + sendTree(); + } + if (state.frame % STATS_EVERY === 0) + sendStats(); + } + function flushInspectReport() { + const id = state.inspectReportId; + if (id == null) + return; + const ops = state.ops; + if (!ops?.debugRectXY || !ops.debugRectWH) { + state.inspectReportId = null; + return; + } + const xy = ops.debugRectXY(); + if (xy === -1) { + if (state.hostCalls - state.inspectAskedAt > 60) { + state.inspectReportId = null; + send({ + t: "inspect", + id, + rect: null + }); + } + return; + } + const wh = ops.debugRectWH(); + state.inspectReportId = null; + send({ + t: "inspect", + id, + rect: [xy << 16 >> 16, xy >> 16, wh & 65535, wh >> 16 & 65535] + }); + } + function sendStats() { + send({ + t: "stats", + frame: state.frame, + nodes: countNodes(rootMirror), + tapeLen: state.tapeLen, + paused: state.paused + }); + } + function sendTree() { + state.treeDirty = false; + state.treeSentAt = state.frame; + send({ + t: "tree", + frame: state.frame, + root: serializeNode(rootMirror) + }); + } + function isTreeMirror(value) { + if (value == null || typeof value !== "object") + return false; + const candidate = value; + return typeof candidate.id === "number" && typeof candidate.type === "number"; + } + function forEachTreeMirror(value, visit) { + if (Array.isArray(value)) { + for (const entry of value) + forEachTreeMirror(entry, visit); + return; + } + if (isTreeMirror(value)) { + visit(value); + return; + } + if (value != null && typeof value === "object") { + const nodes = value.nodes; + if (nodes !== undefined) + forEachTreeMirror(nodes, visit); + } + } + function forEachTreeChild(node, visit) { + const children2 = Array.isArray(node.children) ? node.children : []; + for (const child of children2) + forEachTreeMirror(child, visit); + } + function serializeNode(node) { + const out = { + i: node.id, + t: node.domTag ?? String(node.type) + }; + if (node.debugName) + out.n = node.debugName; + const cls = node.domAttrs?.class; + if (typeof cls === "string" && cls) + out.c = cls; + if (node.text) + out.x = node.text.length > 80 ? node.text.slice(0, 79) + "…" : node.text; + const kids = []; + forEachTreeChild(node, (child) => { + if (child.domNodeType === 8) + return; + kids.push(serializeNode(child)); + }); + if (kids.length) + out.k = kids; + return out; + } + function countNodes(node) { + let n = 1; + forEachTreeChild(node, (child) => { + n += countNodes(child); + }); + return n; + } + function bridgeConsole() { + const g = globalThis; + if (!g.console) + g.console = {}; + const c = g.console; + if (c.__pocketBridged) + return; + c.__pocketBridged = true; + for (const level of ["log", "warn", "error"]) { + const original = c[level]; + c[level] = (...args) => { + send({ + t: "log", + level, + args: args.map((a) => fmt(a)) + }); + original?.apply(c, args); + }; + } + } + function fmt(v, depth = 0) { + if (v === undefined) + return "undefined"; + if (v === null) + return "null"; + const t = typeof v; + if (t === "string") { + const s = v; + return depth === 0 ? clip(s) : JSON.stringify(clip(s)); + } + if (t === "number" || t === "boolean" || t === "bigint") + return String(v); + if (t === "function") { + const name = v.name; + return name ? `[function ${name}]` : "[function]"; + } + if (depth >= 3) + return Array.isArray(v) ? "[…]" : "{…}"; + if (Array.isArray(v)) { + const items = v.slice(0, 20).map((x) => fmt(x, depth + 1)); + if (v.length > 20) + items.push(`… ${v.length - 20} more`); + return `[${items.join(", ")}]`; + } + if (v instanceof Error) + return `${v.name}: ${v.message}`; + const entries2 = Object.entries(v).slice(0, 20); + const body = entries2.map(([k, x]) => `${k}: ${fmt(x, depth + 1)}`).join(", "); + return `{${body}}`; + } + function clip(s) { + return s.length > 200 ? s.slice(0, 199) + "…" : s; + } + function hostKind() { + const ops = state.ops; + if (typeof ops?.__host === "string") + return ops.__host; + if (ops?.__textures !== undefined) + return "psp"; + if (typeof globalThis.document !== "undefined") + return "web"; + return "headless"; + } + var api = { + get frame() { + return state.frame; + }, + dumpTape: () => exportTape(), + replay: (tape) => { + state.replayMasks = expandTape(tape); + state.replayAnalog = tape.analog ? expandTapeAnalog(tape) : null; + state.replayAt = 0; + } + }; + + // framework/src/styles.ts + var verbatim = new Map; + var sortedAlias = new Map; + function normalize(cls) { + return cls.trim().replace(/\s+/g, " "); + } + function sortTokens(normalized) { + return normalized.split(" ").sort().join(" "); + } + var ALIAS_AMBIGUOUS = -1; + function registerStyles(table) { + for (const key of Object.keys(table)) { + const id = table[key]; + const norm = normalize(key); + verbatim.set(norm, id); + const sorted = sortTokens(norm); + const prev = sortedAlias.get(sorted); + sortedAlias.set(sorted, prev !== undefined && prev !== id ? ALIAS_AMBIGUOUS : id); + } + } + function resolveStyle(cls) { + const norm = normalize(cls); + const hit = verbatim.get(norm); + if (hit !== undefined) + return hit; + const alias = sortedAlias.get(sortTokens(norm)); + return alias === ALIAS_AMBIGUOUS ? undefined : alias; + } + + // framework/src/touch.ts + var LEGACY_COORD_BITS = 9; + var LEGACY_COORD_MASK = (1 << LEGACY_COORD_BITS) - 1; + var LEGACY_ID_SHIFT = LEGACY_COORD_BITS * 2; + var WIDE_MARKER = 2147483648; + var WIDE_COORD_BITS = 10; + var WIDE_COORD_MASK = (1 << WIDE_COORD_BITS) - 1; + var WIDE_ID_SHIFT = WIDE_COORD_BITS * 2; + var EMPTY = Object.freeze([]); + var snapshot = EMPTY; + function __setTouches(packed) { + if (!packed || packed.length === 0) { + snapshot = EMPTY; + return; + } + snapshot = Object.freeze(packed.slice(0, 8).map((value) => { + const wide = (value & WIDE_MARKER) !== 0; + const coordBits = wide ? WIDE_COORD_BITS : LEGACY_COORD_BITS; + const coordMask = wide ? WIDE_COORD_MASK : LEGACY_COORD_MASK; + const idShift = wide ? WIDE_ID_SHIFT : LEGACY_ID_SHIFT; + return Object.freeze({ + id: value >>> idShift & 255, + x: value & coordMask, + y: value >>> coordBits & coordMask + }); + })); + } + function __resetTouches() { + snapshot = EMPTY; + } + + // framework/src/effects.ts + var nextId = 1; + var pending = new Map; + var queue = []; + function traceSink() { + const s = globalThis.__pocketEffectTrace; + return typeof s === "function" ? s : null; + } + function resetEffects() { + nextId = 1; + pending.clear(); + queue = []; + } + function __drainEffects() { + if (queue.length === 0) + return; + const batch = queue; + queue = []; + for (const { + id, + result + } of batch) { + const entry = pending.get(id); + if (!entry) + continue; + pending.delete(id); + traceSink()?.({ + t: "delivery", + frame: virtualFrame(), + id, + kind: entry.kind + }); + entry.onResult(result); + } + } + + // framework/src/styles.generated.ts + var STYLE_IDS = { + "flex-col gap-1 p-3 w-[136] rounded-xl shadow-md overflow-hidden bg-white border-slate-200 translate-y-1 focus:bg-blue-50 focus:border-blue-500 focus:translate-y-0 transition-all duration-150 ease-out": 0, + "h-1 w-full rounded-sm bg-gradient-to-r from-blue-500 to-blue-600": 1, + "w-1 h-7 bg-blue-500": 2, + "flex-col gap-1 p-3 w-[136] rounded-xl shadow-md overflow-hidden bg-white border-slate-200 translate-y-1 focus:bg-emerald-50 focus:border-emerald-500 focus:translate-y-0 transition-all duration-150 ease-out": 3, + "h-1 w-full rounded-sm bg-gradient-to-r from-emerald-500 to-emerald-600": 4, + "w-1 h-7 bg-emerald-500": 5, + "flex-col gap-1 p-3 w-[136] rounded-xl shadow-md overflow-hidden bg-white border-slate-200 translate-y-1 focus:bg-amber-50 focus:border-amber-500 focus:translate-y-0 transition-all duration-150 ease-out": 6, + "h-1 w-full rounded-sm bg-gradient-to-r from-amber-500 to-amber-600": 7, + "w-1 h-7 bg-amber-500": 8, + "flex-row items-center gap-3 p-3 rounded-xl shadow-md bg-white border-slate-200": 9, + "flex-col gap-1": 10, + "text-sm text-slate-950 font-bold": 11, + "text-xs text-slate-600": 12, + "relative flex-col w-full h-full p-4 gap-3 bg-slate-50 overflow-hidden": 13, + "absolute left-0 top-[58] w-64 h-1 rounded-full opacity-50 bg-gradient-to-r from-blue-300 to-transparent": 14, + "absolute left-[210] top-[246] w-56 h-1 rounded-full opacity-40 bg-gradient-to-l from-cyan-300 to-transparent": 15, + "flex-row items-end justify-between": 16, + "flex-col": 17, + "text-xs text-blue-600 tracking-wide": 18, + "text-2xl text-slate-950 font-bold": 19, + "text-xs text-slate-500": 20, + "flex-row gap-3": 21, + "grow flex-col": 22, + "relative flex-col w-full h-full bg-slate-50 overflow-hidden": 23, + "absolute inset-0 z-50 flex-col items-center justify-center": 24, + "absolute inset-0 bg-slate-950": 25, + "flex-col gap-2 w-[328] p-3 rounded-xl shadow-lg bg-white border-slate-200": 26, + "absolute left-3 right-3 bottom-3 flex-row items-center justify-between px-2 py-1 rounded-lg shadow-md bg-white border-slate-200": 27, + "flex-row flex-wrap": 28, + grow: 29 + }; + + // framework/src/index.ts + if (typeof globalThis.queueMicrotask !== "function") { + globalThis.queueMicrotask = (fn) => { + Promise.resolve().then(fn); + }; + } + var STYLES_KEY = "ui:styles"; + var FONT_PREFIX = "ui:font."; + var IMG_PREFIX = "ui:img."; + var SPRITE_PREFIX = "ui:sprite."; + function globalOps() { + return globalThis.ui; + } + function uploadPakImages(ops) { + if (ops.__textures) + return; + for (const key of entries(IMG_PREFIX)) { + const blob = get(key); + let handle; + if (ops.uploadImgEntry) { + handle = ops.uploadImgEntry(blob); + } else { + const dv = new DataView(blob.buffer, blob.byteOffset, blob.byteLength); + handle = ops.uploadTexture(blob.subarray(8), dv.getUint16(0, true), dv.getUint16(2, true), blob[4]); + } + if (handle >= 0) + registerTexture(key.slice(IMG_PREFIX.length), handle); + } + } + function uploadPakSprites(ops) { + if (ops.__sprites) + return; + for (const key of entries(SPRITE_PREFIX)) { + const blob = get(key); + const dv = new DataView(blob.buffer, blob.byteOffset, blob.byteLength); + const w = dv.getUint16(0, true); + const h = dv.getUint16(2, true); + const psm = blob[4]; + const frames = dv.getUint16(6, true); + const cols = dv.getUint16(8, true); + const step = dv.getUint16(10, true); + const handle = ops.uploadTexture(blob.subarray(16), w, h, psm); + if (handle >= 0) { + registerSprite(key.slice(SPRITE_PREFIX.length), { + handle, + frames, + cols, + step + }); + } + } + } + function createLayer(style) { + const layer = createElement2("view"); + setProp(layer, "style", style, undefined); + return layer; + } + var appLayer = null; + var overlayLayer = null; + function resizeViewport(w, h) { + if (!appLayer || !overlayLayer) + return; + setProp(appLayer, "style", { + width: w, + height: h, + overflow: ENUMS.Overflow.Hidden + }, undefined); + setProp(overlayLayer, "style", { + width: w, + height: h, + posType: ENUMS.PosType.Absolute, + insetT: 0, + insetR: 0, + insetB: 0, + insetL: 0, + zIndex: 1000 + }, undefined); + const ops = getOps(); + ops.__viewport = { + w, + h + }; + } + function render2(code, opts = {}) { + const host = detectHost(opts.ops); + installHost(host); + setStyleResolver(resolveStyle); + if (opts.styles) + registerStyles(opts.styles); + const nativeTextureTable = host.kind === "native" ? host.ops.__textures : undefined; + if (host.kind === "native") { + if (nativeTextureTable) { + for (const key in nativeTextureTable) { + registerTexture(key, nativeTextureTable[key]); + } + } + const spr = host.ops.__sprites; + if (spr) { + for (const key in spr) + registerSprite(key, spr[key]); + } + } + if (host.kind === "injected" || nativeTextureTable === undefined) { + if (opts.pak) + loadPack(opts.pak); + if (hasPack()) { + for (const key of entries()) { + if (key === STYLES_KEY) { + host.ops.loadStyles?.(get(key)); + } else if (key.startsWith(FONT_PREFIX)) { + host.ops.loadFontAtlas?.(get(key)); + } + } + } + } + const viewport = hostViewport(host.ops); + const layerW = viewport?.w ?? SCREEN_W; + const layerH = viewport?.h ?? SCREEN_H; + const appRoot = createLayer({ + width: layerW, + height: layerH, + overflow: ENUMS.Overflow.Hidden + }); + const overlayRoot2 = createLayer({ + width: layerW, + height: layerH, + posType: ENUMS.PosType.Absolute, + insetT: 0, + insetR: 0, + insetB: 0, + insetL: 0, + zIndex: 1000 + }); + insertNode(rootMirror, appRoot); + insertNode(rootMirror, overlayRoot2); + setOverlayRoot(overlayRoot2); + appLayer = appRoot; + overlayLayer = overlayRoot2; + setInputRoot(appRoot); + setHitRoot(rootMirror); + resetFrameHooks(); + resetClock(); + resetEffects(); + initDevtools(host.ops); + installFrameHandler(wrapFrameHandler((buttons, analog, touches) => { + __advanceClock(); + __setAnalog(analog); + __setTouches(touches); + __drainEffects(); + runFrameHooks(buttons); + handleFrame(buttons); + runSweep(); + })); + const dispose = render(code, appRoot); + const removeResizeViewportHook = installResizeViewportHook(resizeViewport); + return () => { + removeResizeViewportHook(); + __resetTouches(); + dispose(); + setInputRoot(null); + setHitRoot(null); + setOverlayRoot(null); + appLayer = null; + overlayLayer = null; + for (const child of rootMirror.children.splice(0)) { + child.parent = null; + host.ops.destroyNode(child.id); + } + runSweep(); + }; + } + function mount(code, opts = {}) { + const ops = opts.ops ?? globalOps(); + if (!ops) { + throw new Error("PocketJS: mount() requires globalThis.ui or opts.ops"); + } + if (opts.pak) + loadPack(opts.pak); + uploadPakImages(ops); + uploadPakSprites(ops); + const dispose = render2(code, { + ops, + styles: opts.styles ?? STYLE_IDS, + pak: opts.pak + }); + return dispose; + } + + // apps/cards/main.tsx + mount(() => createComponent2(Cards, {})); +})(); diff --git a/hosts/wm6/vs2005/prebuilt/PocketJS.WM6.Cards.pak b/hosts/wm6/vs2005/prebuilt/PocketJS.WM6.Cards.pak new file mode 100644 index 000000000..e18e2a472 Binary files /dev/null and b/hosts/wm6/vs2005/prebuilt/PocketJS.WM6.Cards.pak differ diff --git a/hosts/wm6/vs2005/prebuilt/PocketJS.WM6.Demo.js b/hosts/wm6/vs2005/prebuilt/PocketJS.WM6.Demo.js new file mode 100644 index 000000000..4d36a7f5c --- /dev/null +++ b/hosts/wm6/vs2005/prebuilt/PocketJS.WM6.Demo.js @@ -0,0 +1,3724 @@ +/* PocketJS WM6 native Rust core + real apps/hero bundle. */ +(() => { + // node_modules/solid-js/dist/solid.js + var sharedConfig = { + context: undefined, + registry: undefined, + effects: undefined, + done: false, + getContextId() { + return getContextId(this.context.count); + }, + getNextContextId() { + return getContextId(this.context.count++); + } + }; + function getContextId(count) { + const num = String(count), len = num.length - 1; + return sharedConfig.context.id + (len ? String.fromCharCode(96 + len) : "") + num; + } + function setHydrateContext(context) { + sharedConfig.context = context; + } + function nextHydrateContext() { + return { + ...sharedConfig.context, + id: sharedConfig.getNextContextId(), + count: 0 + }; + } + var IS_DEV = false; + var equalFn = (a, b) => a === b; + var $PROXY = Symbol("solid-proxy"); + var SUPPORTS_PROXY = typeof Proxy === "function"; + var $TRACK = Symbol("solid-track"); + var $DEVCOMP = Symbol("solid-dev-component"); + var signalOptions = { + equals: equalFn + }; + var ERROR = null; + var runEffects = runQueue; + var STALE = 1; + var PENDING = 2; + var UNOWNED = { + owned: null, + cleanups: null, + context: null, + owner: null + }; + var Owner = null; + var Transition = null; + var Scheduler = null; + var ExternalSourceConfig = null; + var Listener = null; + var Updates = null; + var Effects = null; + var ExecCount = 0; + function createRoot(fn, detachedOwner) { + const listener = Listener, owner = Owner, unowned = fn.length === 0, current = detachedOwner === undefined ? owner : detachedOwner, root = unowned ? UNOWNED : { + owned: null, + cleanups: null, + context: current ? current.context : null, + owner: current + }, updateFn = unowned ? fn : () => fn(() => untrack(() => cleanNode(root))); + Owner = root; + Listener = null; + try { + return runUpdates(updateFn, true); + } finally { + Listener = listener; + Owner = owner; + } + } + function createSignal(value, options) { + options = options ? Object.assign({}, signalOptions, options) : signalOptions; + const s = { + value, + observers: null, + observerSlots: null, + comparator: options.equals || undefined + }; + const setter = (value2) => { + if (typeof value2 === "function") { + if (Transition && Transition.running && Transition.sources.has(s)) + value2 = value2(s.tValue); + else + value2 = value2(s.value); + } + return writeSignal(s, value2); + }; + return [readSignal.bind(s), setter]; + } + function createRenderEffect(fn, value, options) { + const c = createComputation(fn, value, false, STALE); + if (Scheduler && Transition && Transition.running) + Updates.push(c); + else + updateComputation(c); + } + function createEffect(fn, value, options) { + runEffects = runUserEffects; + const c = createComputation(fn, value, false, STALE), s = SuspenseContext && useContext(SuspenseContext); + if (s) + c.suspense = s; + if (!options || !options.render) + c.user = true; + Effects ? Effects.push(c) : updateComputation(c); + } + function createMemo(fn, value, options) { + options = options ? Object.assign({}, signalOptions, options) : signalOptions; + const c = createComputation(fn, value, true, 0); + c.observers = null; + c.observerSlots = null; + c.comparator = options.equals || undefined; + if (Scheduler && Transition && Transition.running) { + c.tState = STALE; + Updates.push(c); + } else + updateComputation(c); + return readSignal.bind(c); + } + function untrack(fn) { + if (!ExternalSourceConfig && Listener === null) + return fn(); + const listener = Listener; + Listener = null; + try { + if (ExternalSourceConfig) + return ExternalSourceConfig.untrack(fn); + return fn(); + } finally { + Listener = listener; + } + } + function onMount(fn) { + createEffect(() => untrack(fn)); + } + function onCleanup(fn) { + if (Owner === null) + ; + else if (Owner.cleanups === null) + Owner.cleanups = [fn]; + else + Owner.cleanups.push(fn); + return fn; + } + function startTransition(fn) { + if (Transition && Transition.running) { + fn(); + return Transition.done; + } + const l = Listener; + const o = Owner; + return Promise.resolve().then(() => { + Listener = l; + Owner = o; + let t; + if (Scheduler || SuspenseContext) { + t = Transition || (Transition = { + sources: new Set, + effects: [], + promises: new Set, + disposed: new Set, + queue: new Set, + running: true + }); + t.done || (t.done = new Promise((res) => t.resolve = res)); + t.running = true; + } + runUpdates(fn, false); + Listener = Owner = null; + return t ? t.done : undefined; + }); + } + var [transPending, setTransPending] = /* @__PURE__ */ createSignal(false); + function useContext(context) { + let value; + return Owner && Owner.context && (value = Owner.context[context.id]) !== undefined ? value : context.defaultValue; + } + var SuspenseContext; + function readSignal() { + const runningTransition = Transition && Transition.running; + if (this.sources && (runningTransition ? this.tState : this.state)) { + if ((runningTransition ? this.tState : this.state) === STALE) + updateComputation(this); + else { + const updates = Updates; + Updates = null; + runUpdates(() => lookUpstream(this), false); + Updates = updates; + } + } + if (Listener) { + const observers = this.observers; + if (!observers || observers[observers.length - 1] !== Listener) { + const sSlot = observers ? observers.length : 0; + if (!Listener.sources) { + Listener.sources = [this]; + Listener.sourceSlots = [sSlot]; + } else { + Listener.sources.push(this); + Listener.sourceSlots.push(sSlot); + } + if (!observers) { + this.observers = [Listener]; + this.observerSlots = [Listener.sources.length - 1]; + } else { + observers.push(Listener); + this.observerSlots.push(Listener.sources.length - 1); + } + } + } + if (runningTransition && Transition.sources.has(this)) + return this.tValue; + return this.value; + } + function writeSignal(node, value, isComp) { + let current = Transition && Transition.running && Transition.sources.has(node) ? node.tValue : node.value; + if (!node.comparator || !node.comparator(current, value)) { + if (Transition) { + const TransitionRunning = Transition.running; + if (TransitionRunning || !isComp && Transition.sources.has(node)) { + Transition.sources.add(node); + node.tValue = value; + } + if (!TransitionRunning) + node.value = value; + } else + node.value = value; + if (node.observers && node.observers.length) { + runUpdates(() => { + for (let i = 0;i < node.observers.length; i += 1) { + const o = node.observers[i]; + const TransitionRunning = Transition && Transition.running; + if (TransitionRunning && Transition.disposed.has(o)) + continue; + if (TransitionRunning ? !o.tState : !o.state) { + if (o.pure) + Updates.push(o); + else + Effects.push(o); + if (o.observers) + markDownstream(o); + } + if (!TransitionRunning) + o.state = STALE; + else + o.tState = STALE; + } + if (Updates.length > 1e6) { + Updates = []; + if (IS_DEV) + ; + throw new Error; + } + }, false); + } + } + return value; + } + function updateComputation(node) { + if (!node.fn) + return; + cleanNode(node); + const time = ExecCount; + runComputation(node, Transition && Transition.running && Transition.sources.has(node) ? node.tValue : node.value, time); + if (Transition && !Transition.running && Transition.sources.has(node)) { + queueMicrotask(() => { + runUpdates(() => { + Transition && (Transition.running = true); + Listener = Owner = node; + runComputation(node, node.tValue, time); + Listener = Owner = null; + }, false); + }); + } + } + function runComputation(node, value, time) { + let nextValue; + const owner = Owner, listener = Listener; + Listener = Owner = node; + try { + nextValue = node.fn(value); + } catch (err) { + if (node.pure) { + if (Transition && Transition.running) { + node.tState = STALE; + node.tOwned && node.tOwned.forEach(cleanNode); + node.tOwned = undefined; + } else { + node.state = STALE; + node.owned && node.owned.forEach(cleanNode); + node.owned = null; + } + } + node.updatedAt = time + 1; + return handleError(err); + } finally { + Listener = listener; + Owner = owner; + } + if (!node.updatedAt || node.updatedAt <= time) { + if (node.updatedAt != null && "observers" in node) { + writeSignal(node, nextValue, true); + } else if (Transition && Transition.running && node.pure) { + if (!Transition.sources.has(node)) + node.value = nextValue; + Transition.sources.add(node); + node.tValue = nextValue; + } else + node.value = nextValue; + node.updatedAt = time; + } + } + function createComputation(fn, init, pure, state = STALE, options) { + const c = { + fn, + state, + updatedAt: null, + owned: null, + sources: null, + sourceSlots: null, + cleanups: null, + value: init, + owner: Owner, + context: Owner ? Owner.context : null, + pure + }; + if (Transition && Transition.running) { + c.state = 0; + c.tState = state; + } + if (Owner === null) + ; + else if (Owner !== UNOWNED) { + if (Transition && Transition.running && Owner.pure) { + if (!Owner.tOwned) + Owner.tOwned = [c]; + else + Owner.tOwned.push(c); + } else { + if (!Owner.owned) + Owner.owned = [c]; + else + Owner.owned.push(c); + } + } + if (ExternalSourceConfig && c.fn) { + const sourceFn = c.fn; + const [track, trigger] = createSignal(undefined, { + equals: false + }); + const ordinary = ExternalSourceConfig.factory(sourceFn, trigger); + onCleanup(() => ordinary.dispose()); + let inTransition; + const triggerInTransition = () => startTransition(trigger).then(() => { + if (inTransition) { + inTransition.dispose(); + inTransition = undefined; + } + }); + c.fn = (x) => { + track(); + if (Transition && Transition.running) { + if (!inTransition) + inTransition = ExternalSourceConfig.factory(sourceFn, triggerInTransition); + return inTransition.track(x); + } + return ordinary.track(x); + }; + } + return c; + } + function runTop(node) { + const runningTransition = Transition && Transition.running; + if ((runningTransition ? node.tState : node.state) === 0) + return; + if ((runningTransition ? node.tState : node.state) === PENDING) + return lookUpstream(node); + if (node.suspense && untrack(node.suspense.inFallback)) + return node.suspense.effects.push(node); + const ancestors = [node]; + while ((node = node.owner) && (!node.updatedAt || node.updatedAt < ExecCount)) { + if (runningTransition && Transition.disposed.has(node)) + return; + if (runningTransition ? node.tState : node.state) + ancestors.push(node); + } + for (let i = ancestors.length - 1;i >= 0; i--) { + node = ancestors[i]; + if (runningTransition) { + let top = node, prev = ancestors[i + 1]; + while ((top = top.owner) && top !== prev) { + if (Transition.disposed.has(top)) + return; + } + } + if ((runningTransition ? node.tState : node.state) === STALE) { + updateComputation(node); + } else if ((runningTransition ? node.tState : node.state) === PENDING) { + const updates = Updates; + Updates = null; + runUpdates(() => lookUpstream(node, ancestors[0]), false); + Updates = updates; + } + } + } + function runUpdates(fn, init) { + if (Updates) + return fn(); + let wait = false; + if (!init) + Updates = []; + if (Effects) + wait = true; + else + Effects = []; + ExecCount++; + try { + const res = fn(); + completeUpdates(wait); + return res; + } catch (err) { + if (!wait) + Effects = null; + Updates = null; + handleError(err); + } + } + function completeUpdates(wait) { + if (Updates) { + if (Scheduler && Transition && Transition.running) + scheduleQueue(Updates); + else + runQueue(Updates); + Updates = null; + } + if (wait) + return; + let res; + if (Transition) { + if (!Transition.promises.size && !Transition.queue.size) { + const sources = Transition.sources; + const disposed = Transition.disposed; + Effects.push.apply(Effects, Transition.effects); + res = Transition.resolve; + for (const e2 of Effects) { + "tState" in e2 && (e2.state = e2.tState); + delete e2.tState; + } + Transition = null; + runUpdates(() => { + for (const d of disposed) + cleanNode(d); + for (const v of sources) { + v.value = v.tValue; + if (v.owned) { + for (let i = 0, len = v.owned.length;i < len; i++) + cleanNode(v.owned[i]); + } + if (v.tOwned) + v.owned = v.tOwned; + delete v.tValue; + delete v.tOwned; + v.tState = 0; + } + setTransPending(false); + }, false); + } else if (Transition.running) { + Transition.running = false; + Transition.effects.push.apply(Transition.effects, Effects); + Effects = null; + setTransPending(true); + return; + } + } + const e = Effects; + Effects = null; + if (e.length) + runUpdates(() => runEffects(e), false); + if (res) + res(); + } + function runQueue(queue) { + for (let i = 0;i < queue.length; i++) + runTop(queue[i]); + } + function scheduleQueue(queue) { + for (let i = 0;i < queue.length; i++) { + const item = queue[i]; + const tasks = Transition.queue; + if (!tasks.has(item)) { + tasks.add(item); + Scheduler(() => { + tasks.delete(item); + runUpdates(() => { + Transition.running = true; + runTop(item); + }, false); + Transition && (Transition.running = false); + }); + } + } + } + function runUserEffects(queue) { + let i, userLength = 0; + for (i = 0;i < queue.length; i++) { + const e = queue[i]; + if (!e.user) + runTop(e); + else + queue[userLength++] = e; + } + if (sharedConfig.context) { + if (sharedConfig.count) { + sharedConfig.effects || (sharedConfig.effects = []); + sharedConfig.effects.push(...queue.slice(0, userLength)); + return; + } + setHydrateContext(); + } + if (sharedConfig.effects && (sharedConfig.done || !sharedConfig.count)) { + queue = [...sharedConfig.effects, ...queue]; + userLength += sharedConfig.effects.length; + delete sharedConfig.effects; + } + for (i = 0;i < userLength; i++) + runTop(queue[i]); + } + function lookUpstream(node, ignore) { + const runningTransition = Transition && Transition.running; + if (runningTransition) + node.tState = 0; + else + node.state = 0; + for (let i = 0;i < node.sources.length; i += 1) { + const source = node.sources[i]; + if (source.sources) { + const state = runningTransition ? source.tState : source.state; + if (state === STALE) { + if (source !== ignore && (!source.updatedAt || source.updatedAt < ExecCount)) + runTop(source); + } else if (state === PENDING) + lookUpstream(source, ignore); + } + } + } + function markDownstream(node) { + const runningTransition = Transition && Transition.running; + for (let i = 0;i < node.observers.length; i += 1) { + const o = node.observers[i]; + if (runningTransition ? !o.tState : !o.state) { + if (runningTransition) + o.tState = PENDING; + else + o.state = PENDING; + if (o.pure) + Updates.push(o); + else + Effects.push(o); + o.observers && markDownstream(o); + } + } + } + function cleanNode(node) { + let i; + if (node.sources) { + while (node.sources.length) { + const source = node.sources.pop(), index = node.sourceSlots.pop(), obs = source.observers; + if (obs && obs.length) { + const n = obs.pop(), s = source.observerSlots.pop(); + if (index < obs.length) { + n.sourceSlots[s] = index; + obs[index] = n; + source.observerSlots[index] = s; + } + } + } + } + if (node.tOwned) { + for (i = node.tOwned.length - 1;i >= 0; i--) + cleanNode(node.tOwned[i]); + delete node.tOwned; + } + if (Transition && Transition.running && node.pure) { + reset(node, true); + } else if (node.owned) { + for (i = node.owned.length - 1;i >= 0; i--) + cleanNode(node.owned[i]); + node.owned = null; + } + if (node.cleanups) { + for (i = node.cleanups.length - 1;i >= 0; i--) + node.cleanups[i](); + node.cleanups = null; + } + if (Transition && Transition.running) + node.tState = 0; + else + node.state = 0; + } + function reset(node, top) { + if (!top) { + node.tState = 0; + Transition.disposed.add(node); + } + if (node.owned) { + for (let i = 0;i < node.owned.length; i++) + reset(node.owned[i]); + } + } + function castError(err) { + if (err instanceof Error) + return err; + return new Error(typeof err === "string" ? err : "Unknown error", { + cause: err + }); + } + function runErrors(err, fns, owner) { + try { + for (const f of fns) + f(err); + } catch (e) { + handleError(e, owner && owner.owner || null); + } + } + function handleError(err, owner = Owner) { + const fns = ERROR && owner && owner.context && owner.context[ERROR]; + const error = castError(err); + if (!fns) + throw error; + if (Effects) + Effects.push({ + fn() { + runErrors(error, fns, owner); + }, + state: STALE + }); + else + runErrors(error, fns, owner); + } + var FALLBACK = Symbol("fallback"); + var hydrationEnabled = false; + function createComponent(Comp, props) { + if (hydrationEnabled) { + if (sharedConfig.context) { + const c = sharedConfig.context; + setHydrateContext(nextHydrateContext()); + const r = untrack(() => Comp(props || {})); + setHydrateContext(c); + return r; + } + } + return untrack(() => Comp(props || {})); + } + function trueFn() { + return true; + } + var propTraps = { + get(_, property, receiver) { + if (property === $PROXY) + return receiver; + return _.get(property); + }, + has(_, property) { + if (property === $PROXY) + return true; + return _.has(property); + }, + set: trueFn, + deleteProperty: trueFn, + getOwnPropertyDescriptor(_, property) { + return { + configurable: true, + enumerable: true, + get() { + return _.get(property); + }, + set: trueFn, + deleteProperty: trueFn + }; + }, + ownKeys(_) { + return _.keys(); + } + }; + function resolveSource(s) { + return !(s = typeof s === "function" ? s() : s) ? {} : s; + } + function resolveSources() { + for (let i = 0, length = this.length;i < length; ++i) { + const v = this[i](); + if (v !== undefined) + return v; + } + } + function mergeProps(...sources) { + let proxy = false; + for (let i = 0;i < sources.length; i++) { + const s = sources[i]; + proxy = proxy || !!s && $PROXY in s; + sources[i] = typeof s === "function" ? (proxy = true, createMemo(s)) : s; + } + if (SUPPORTS_PROXY && proxy) { + return new Proxy({ + get(property) { + for (let i = sources.length - 1;i >= 0; i--) { + const v = resolveSource(sources[i])[property]; + if (v !== undefined) + return v; + } + }, + has(property) { + for (let i = sources.length - 1;i >= 0; i--) { + if (property in resolveSource(sources[i])) + return true; + } + return false; + }, + keys() { + const keys = []; + for (let i = 0;i < sources.length; i++) + keys.push(...Object.keys(resolveSource(sources[i]))); + return [...new Set(keys)]; + } + }, propTraps); + } + const sourcesMap = {}; + const defined = Object.create(null); + for (let i = sources.length - 1;i >= 0; i--) { + const source = sources[i]; + if (!source) + continue; + const sourceKeys = Object.getOwnPropertyNames(source); + for (let i2 = sourceKeys.length - 1;i2 >= 0; i2--) { + const key = sourceKeys[i2]; + if (key === "__proto__" || key === "constructor") + continue; + const desc = Object.getOwnPropertyDescriptor(source, key); + if (!defined[key]) { + defined[key] = desc.get ? { + enumerable: true, + configurable: true, + get: resolveSources.bind(sourcesMap[key] = [desc.get.bind(source)]) + } : desc.value !== undefined ? desc : undefined; + } else { + const sources2 = sourcesMap[key]; + if (sources2) { + if (desc.get) + sources2.push(desc.get.bind(source)); + else if (desc.value !== undefined) + sources2.push(() => desc.value); + } + } + } + } + const target = {}; + const definedKeys = Object.keys(defined); + for (let i = definedKeys.length - 1;i >= 0; i--) { + const key = definedKeys[i], desc = defined[key]; + if (desc && desc.get) + Object.defineProperty(target, key, desc); + else + target[key] = desc ? desc.value : undefined; + } + return target; + } + var narrowedError = (name) => `Stale read from <${name}>.`; + function Show(props) { + const keyed = props.keyed; + const conditionValue = createMemo(() => props.when, undefined, undefined); + const condition = keyed ? conditionValue : createMemo(conditionValue, undefined, { + equals: (a, b) => !a === !b + }); + return createMemo(() => { + const c = condition(); + if (c) { + const child = props.children; + const fn = typeof child === "function" && child.length > 0; + return fn ? untrack(() => child(keyed ? c : () => { + if (!untrack(condition)) + throw narrowedError("Show"); + return conditionValue(); + })) : child; + } + return props.fallback; + }, undefined, undefined); + } + + // node_modules/solid-js/universal/dist/universal.js + var memo = (fn) => createMemo(() => fn()); + function createRenderer$1({ + createElement, + createTextNode, + isTextNode, + replaceText, + insertNode, + removeNode, + setProperty, + getParentNode, + getFirstChild, + getNextSibling + }) { + function insert(parent, accessor, marker, initial) { + if (marker !== undefined && !initial) + initial = []; + if (typeof accessor !== "function") + return insertExpression(parent, accessor, initial, marker); + createRenderEffect((current) => insertExpression(parent, accessor(), current, marker), initial); + } + function insertExpression(parent, value, current, marker, unwrapArray) { + while (typeof current === "function") + current = current(); + if (value === current) + return current; + const t = typeof value, multi = marker !== undefined; + if (t === "string" || t === "number") { + if (t === "number") + value = value.toString(); + if (multi) { + let node = current[0]; + if (node && isTextNode(node)) { + replaceText(node, value); + } else + node = createTextNode(value); + current = cleanChildren(parent, current, marker, node); + } else { + if (current !== "" && typeof current === "string") { + replaceText(getFirstChild(parent), current = value); + } else { + cleanChildren(parent, current, marker, createTextNode(value)); + current = value; + } + } + } else if (value == null || t === "boolean") { + current = cleanChildren(parent, current, marker); + } else if (t === "function") { + createRenderEffect(() => { + let v = value(); + while (typeof v === "function") + v = v(); + current = insertExpression(parent, v, current, marker); + }); + return () => current; + } else if (Array.isArray(value)) { + const array = []; + if (normalizeIncomingArray(array, value, unwrapArray)) { + createRenderEffect(() => current = insertExpression(parent, array, current, marker, true)); + return () => current; + } + if (array.length === 0) { + const replacement = cleanChildren(parent, current, marker); + if (multi) + return current = replacement; + } else { + if (Array.isArray(current)) { + if (current.length === 0) { + appendNodes(parent, array, marker); + } else + reconcileArrays(parent, current, array); + } else if (current == null || current === "") { + appendNodes(parent, array); + } else { + reconcileArrays(parent, multi && current || [getFirstChild(parent)], array); + } + } + current = array; + } else { + if (Array.isArray(current)) { + if (multi) + return current = cleanChildren(parent, current, marker, value); + cleanChildren(parent, current, null, value); + } else if (current == null || current === "" || !getFirstChild(parent)) { + insertNode(parent, value); + } else + replaceNode(parent, value, getFirstChild(parent)); + current = value; + } + return current; + } + function normalizeIncomingArray(normalized, array, unwrap) { + let dynamic = false; + for (let i = 0, len = array.length;i < len; i++) { + let item = array[i], t; + if (item == null || item === true || item === false) + ; + else if (Array.isArray(item)) { + dynamic = normalizeIncomingArray(normalized, item) || dynamic; + } else if ((t = typeof item) === "string" || t === "number") { + normalized.push(createTextNode(item)); + } else if (t === "function") { + if (unwrap) { + while (typeof item === "function") + item = item(); + dynamic = normalizeIncomingArray(normalized, Array.isArray(item) ? item : [item]) || dynamic; + } else { + normalized.push(item); + dynamic = true; + } + } else + normalized.push(item); + } + return dynamic; + } + function reconcileArrays(parentNode, a, b) { + let bLength = b.length, aEnd = a.length, bEnd = bLength, aStart = 0, bStart = 0, after = getNextSibling(a[aEnd - 1]), map = null; + while (aStart < aEnd || bStart < bEnd) { + if (a[aStart] === b[bStart]) { + aStart++; + bStart++; + continue; + } + while (a[aEnd - 1] === b[bEnd - 1]) { + aEnd--; + bEnd--; + } + if (aEnd === aStart) { + const node = bEnd < bLength ? bStart ? getNextSibling(b[bStart - 1]) : b[bEnd - bStart] : after; + while (bStart < bEnd) + insertNode(parentNode, b[bStart++], node); + } else if (bEnd === bStart) { + while (aStart < aEnd) { + if (!map || !map.has(a[aStart])) + removeNode(parentNode, a[aStart]); + aStart++; + } + } else if (a[aStart] === b[bEnd - 1] && b[bStart] === a[aEnd - 1]) { + const node = getNextSibling(a[--aEnd]); + insertNode(parentNode, b[bStart++], getNextSibling(a[aStart++])); + insertNode(parentNode, b[--bEnd], node); + a[aEnd] = b[bEnd]; + } else { + if (!map) { + map = new Map; + let i = bStart; + while (i < bEnd) + map.set(b[i], i++); + } + const index = map.get(a[aStart]); + if (index != null) { + if (bStart < index && index < bEnd) { + let i = aStart, sequence = 1, t; + while (++i < aEnd && i < bEnd) { + if ((t = map.get(a[i])) == null || t !== index + sequence) + break; + sequence++; + } + if (sequence > index - bStart) { + const node = a[aStart]; + while (bStart < index) + insertNode(parentNode, b[bStart++], node); + } else + replaceNode(parentNode, b[bStart++], a[aStart++]); + } else + aStart++; + } else + removeNode(parentNode, a[aStart++]); + } + } + } + function cleanChildren(parent, current, marker, replacement) { + if (marker === undefined) { + let removed; + while (removed = getFirstChild(parent)) + removeNode(parent, removed); + replacement && insertNode(parent, replacement); + return ""; + } + const node = replacement || createTextNode(""); + if (current.length) { + let inserted = false; + for (let i = current.length - 1;i >= 0; i--) { + const el = current[i]; + if (node !== el) { + const isParent = getParentNode(el) === parent; + if (!inserted && !i) + isParent ? replaceNode(parent, node, el) : insertNode(parent, node, marker); + else + isParent && removeNode(parent, el); + } else + inserted = true; + } + } else + insertNode(parent, node, marker); + return [node]; + } + function appendNodes(parent, array, marker) { + for (let i = 0, len = array.length;i < len; i++) + insertNode(parent, array[i], marker); + } + function replaceNode(parent, newNode, oldNode) { + insertNode(parent, newNode, oldNode); + removeNode(parent, oldNode); + } + function spreadExpression(node, props, prevProps = {}, skipChildren) { + props || (props = {}); + if (!skipChildren) { + createRenderEffect(() => prevProps.children = insertExpression(node, props.children, prevProps.children)); + } + createRenderEffect(() => props.ref && props.ref(node)); + createRenderEffect(() => { + for (const prop in props) { + if (prop === "children" || prop === "ref") + continue; + const value = props[prop]; + if (value === prevProps[prop]) + continue; + setProperty(node, prop, value, prevProps[prop]); + prevProps[prop] = value; + } + }); + return prevProps; + } + return { + render(code, element) { + let disposer; + createRoot((dispose) => { + disposer = dispose; + insert(element, code()); + }); + return disposer; + }, + insert, + spread(node, accessor, skipChildren) { + if (typeof accessor === "function") { + createRenderEffect((current) => spreadExpression(node, accessor(), current, skipChildren)); + } else + spreadExpression(node, accessor, undefined, skipChildren); + }, + createElement, + createTextNode, + insertNode, + setProp(node, name, value, prev) { + setProperty(node, name, value, prev); + return value; + }, + mergeProps, + effect: createRenderEffect, + memo, + createComponent, + use(fn, element, arg) { + return untrack(() => fn(element, arg)); + } + }; + } + function createRenderer(options) { + const renderer = createRenderer$1(options); + renderer.mergeProps = mergeProps; + return renderer; + } + + // contracts/spec/spec.ts + var SCREEN_W = 480; + var SCREEN_H = 272; + var NODE_TYPE = { + view: 0, + text: 1, + image: 2 + }; + var ROOT_ID = 1; + var STYLE_ID_NONE = -1; + var PROP = { + width: 1, + height: 2, + minW: 3, + minH: 4, + maxW: 5, + maxH: 6, + paddingT: 8, + paddingR: 9, + paddingB: 10, + paddingL: 11, + marginT: 12, + marginR: 13, + marginB: 14, + marginL: 15, + gap: 16, + flexDir: 17, + justify: 18, + align: 19, + grow: 20, + shrink: 21, + basis: 22, + flexWrap: 23, + posType: 24, + insetT: 25, + insetR: 26, + insetB: 27, + insetL: 28, + display: 29, + overflow: 30, + zIndex: 31, + bgColor: 64, + gradFrom: 65, + gradTo: 66, + gradDir: 67, + radius: 68, + opacity: 69, + borderColor: 70, + borderWidth: 71, + shadow: 72, + bevelOuterLight: 77, + bevelOuterDark: 78, + bevelInnerLight: 79, + bevelInnerDark: 80, + bevelWidth: 81, + textColor: 96, + fontSlot: 97, + textAlign: 98, + lineHeight: 99, + tracking: 100, + translateX: 128, + translateY: 129, + scale: 130, + rotate: 131, + scaleX: 132, + scaleY: 133, + originX: 134, + originY: 135, + rotateX: 136, + rotateY: 137, + translateZ: 138, + perspective: 139, + arcStart: 140, + arcSweep: 141, + arcWidth: 142 + }; + var ANIMATABLE = [ + "width", + "height", + "paddingT", + "paddingR", + "paddingB", + "paddingL", + "marginT", + "marginR", + "marginB", + "marginL", + "gap", + "basis", + "insetT", + "insetR", + "insetB", + "insetL", + "bgColor", + "gradFrom", + "gradTo", + "radius", + "opacity", + "borderColor", + "borderWidth", + "textColor", + "lineHeight", + "tracking", + "translateX", + "translateY", + "scale", + "rotate", + "scaleX", + "scaleY", + "rotateX", + "rotateY", + "translateZ", + "arcStart", + "arcSweep", + "arcWidth" + ]; + function animBit(prop) { + return ANIMATABLE.indexOf(prop); + } + var VALUE_KIND = { + f32: 0, + color: 1, + int: 2 + }; + var PROP_VALUE_KIND = { + width: VALUE_KIND.f32, + height: VALUE_KIND.f32, + minW: VALUE_KIND.f32, + minH: VALUE_KIND.f32, + maxW: VALUE_KIND.f32, + maxH: VALUE_KIND.f32, + paddingT: VALUE_KIND.f32, + paddingR: VALUE_KIND.f32, + paddingB: VALUE_KIND.f32, + paddingL: VALUE_KIND.f32, + marginT: VALUE_KIND.f32, + marginR: VALUE_KIND.f32, + marginB: VALUE_KIND.f32, + marginL: VALUE_KIND.f32, + gap: VALUE_KIND.f32, + flexDir: VALUE_KIND.int, + justify: VALUE_KIND.int, + align: VALUE_KIND.int, + grow: VALUE_KIND.f32, + shrink: VALUE_KIND.f32, + basis: VALUE_KIND.f32, + flexWrap: VALUE_KIND.int, + posType: VALUE_KIND.int, + insetT: VALUE_KIND.f32, + insetR: VALUE_KIND.f32, + insetB: VALUE_KIND.f32, + insetL: VALUE_KIND.f32, + display: VALUE_KIND.int, + overflow: VALUE_KIND.int, + zIndex: VALUE_KIND.int, + bgColor: VALUE_KIND.color, + gradFrom: VALUE_KIND.color, + gradTo: VALUE_KIND.color, + gradDir: VALUE_KIND.int, + radius: VALUE_KIND.f32, + opacity: VALUE_KIND.f32, + borderColor: VALUE_KIND.color, + borderWidth: VALUE_KIND.f32, + shadow: VALUE_KIND.int, + bevelOuterLight: VALUE_KIND.color, + bevelOuterDark: VALUE_KIND.color, + bevelInnerLight: VALUE_KIND.color, + bevelInnerDark: VALUE_KIND.color, + bevelWidth: VALUE_KIND.f32, + textColor: VALUE_KIND.color, + fontSlot: VALUE_KIND.int, + textAlign: VALUE_KIND.int, + lineHeight: VALUE_KIND.f32, + tracking: VALUE_KIND.f32, + translateX: VALUE_KIND.f32, + translateY: VALUE_KIND.f32, + scale: VALUE_KIND.f32, + rotate: VALUE_KIND.f32, + scaleX: VALUE_KIND.f32, + scaleY: VALUE_KIND.f32, + originX: VALUE_KIND.f32, + originY: VALUE_KIND.f32, + rotateX: VALUE_KIND.f32, + rotateY: VALUE_KIND.f32, + translateZ: VALUE_KIND.f32, + perspective: VALUE_KIND.f32, + arcStart: VALUE_KIND.f32, + arcSweep: VALUE_KIND.f32, + arcWidth: VALUE_KIND.f32 + }; + var ENUMS = { + FlexDir: { + Row: 0, + Col: 1 + }, + Justify: { + Start: 0, + Center: 1, + End: 2, + Between: 3, + Around: 4 + }, + Align: { + Start: 0, + Center: 1, + End: 2, + Stretch: 3 + }, + PosType: { + Relative: 0, + Absolute: 1 + }, + Display: { + Flex: 0, + None: 1 + }, + Overflow: { + Visible: 0, + Hidden: 1 + }, + TextAlign: { + Left: 0, + Center: 1, + Right: 2 + }, + GradDir: { + ToTop: 0, + ToBottom: 1, + ToLeft: 2, + ToRight: 3 + }, + Easing: { + Linear: 0, + EaseIn: 1, + EaseOut: 2, + EaseInOut: 3, + OutBack: 4, + Spring: 5, + SpringBouncy: 6, + CubicBezier: 7 + } + }; + var PSM = { + PSM_5650: 0, + PSM_4444: 2, + PSM_8888: 3, + PSM_T8: 5 + }; + var IMG_FLAG_RLE = 1 << 0; + var IMG_FLAG_LINEAR = 1 << 1; + var TILESET_FLAG_RLE = 1 << 0; + var TILESET_FLAG_LINEAR = 1 << 1; + var SVC_IMG_MAX_BYTES = 128 * 1024; + var STREAM_FLAG_ENDED = 1 << 0; + var STYLE_VARIANT_BASE = 1 << 0; + var STYLE_VARIANT_FOCUS = 1 << 1; + var STYLE_VARIANT_ACTIVE = 1 << 2; + var STYLE_HAS_TRANSITION = 1 << 3; + var STYLE_HAS_ANIMATION = 1 << 4; + var ANIM_FILL_BACKWARDS = 1 << 0; + var ANIM_FILL_FORWARDS = 1 << 1; + function abgr(r, g, b, a = 255) { + return ((a & 255) << 24 | (b & 255) << 16 | (g & 255) << 8 | r & 255) >>> 0; + } + var FONT_FLAG_BOLD = 1 << 0; + var PAK_MAGIC = 1263551300; + var PAK_VERSION = 1; + var PAK_HEADER_SIZE = 32; + var PAK_ENTRY_SIZE = 24; + var BTN = { + SELECT: 1, + START: 8, + UP: 16, + RIGHT: 32, + DOWN: 64, + LEFT: 128, + LTRIGGER: 256, + RTRIGGER: 512, + TRIANGLE: 4096, + CIRCLE: 8192, + CROSS: 16384, + SQUARE: 32768 + }; + var ANALOG_CENTER = 32896; + var FIXED_DT = 1 / 60; + + // framework/src/host.ts + function hostViewport(ops) { + return ops.__viewport ?? null; + } + var current = null; + function embeddedBuildHostContract() { + const target = ""; + const hostAbi = 0; + return target && hostAbi > 0 ? { + target, + hostAbi + } : null; + } + function assertNativeHostContract(ops, expected = embeddedBuildHostContract()) { + if (!expected) + return; + if (typeof ops.__host !== "string") { + throw new Error(`PocketJS: this bundle targets "${expected.target}" but the native host predates platform ` + "contracts — add __host/__hostAbi to its ui namespace (see framework/src/host.ts HostOps)"); + } + if (ops.__host !== expected.target) { + throw new Error(`PocketJS: native target mismatch (bundle=${expected.target}, host=${ops.__host})`); + } + if (ops.__hostAbi !== expected.hostAbi) { + throw new Error(`PocketJS: native host ABI mismatch (bundle=${expected.hostAbi}, host=${ops.__hostAbi ?? "missing"})`); + } + } + function detectHost(injected) { + const native = globalThis.ui; + const nativeMarked = native !== undefined && (typeof native.__host === "string" || native.__textures !== undefined); + if (injected) { + if (native !== undefined && injected === native && nativeMarked) { + assertNativeHostContract(native); + return { + ops: injected, + kind: "native", + target: native.__host ?? "unknown", + strict: false + }; + } + return { + ops: injected, + kind: "injected", + target: injected.__host ?? "injected", + strict: true + }; + } + if (native !== undefined && nativeMarked) { + assertNativeHostContract(native); + return { + ops: native, + kind: "native", + target: native.__host ?? "unknown", + strict: false + }; + } + if (native) { + return { + ops: native, + kind: "injected", + target: "injected", + strict: true + }; + } + throw new Error("PocketJS: no host — pass HostOps to render() (web/test) or run under a native runtime (globalThis.ui)"); + } + function installHost(host) { + current = host; + } + function getHost() { + if (!current) { + throw new Error("PocketJS: host not installed — call render() first"); + } + return current; + } + function getOps() { + return getHost().ops; + } + function installFrameHandler(fn) { + globalThis.frame = fn; + } + function installResizeViewportHook(resizeViewport) { + const globals = globalThis; + const previous = globals.__pocketResizeViewport; + const hook = (width, height) => resizeViewport(width, height); + globals.__pocketResizeViewport = hook; + return () => { + if (globals.__pocketResizeViewport !== hook) + return; + if (previous) + globals.__pocketResizeViewport = previous; + else + delete globals.__pocketResizeViewport; + }; + } + function parseHexColor(s) { + let hex = s.slice(1); + if (hex.length === 3) { + hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; + } + if (hex.length !== 6 && hex.length !== 8) { + throw new Error(`PocketJS: bad color '${s}' (expected #rgb/#rrggbb/#rrggbbaa)`); + } + if (!/^[0-9a-fA-F]+$/.test(hex)) + throw new Error(`PocketJS: bad color '${s}'`); + const n = parseInt(hex, 16); + if (hex.length === 6) { + return abgr(n >>> 16 & 255, n >>> 8 & 255, n & 255, 255); + } + return abgr(n >>> 24 & 255, n >>> 16 & 255, n >>> 8 & 255, n & 255); + } + function encodePropValue(prop, value) { + const kind = PROP_VALUE_KIND[prop]; + if (typeof value === "string") { + if (kind === VALUE_KIND.color) + return parseHexColor(value); + const n = Number(value); + if (Number.isNaN(n)) { + throw new Error(`PocketJS: non-numeric value '${value}' for prop '${prop}'`); + } + value = n; + } + if (kind === VALUE_KIND.color || kind === VALUE_KIND.int) + return value >>> 0; + return value; + } + + // framework/src/clock.ts + var TICKS_PER_SECOND = 60; + var VALID_HZ = [1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60]; + var hz = TICKS_PER_SECOND; + var frame = -1; + var timerSeq = 0; + var timers = []; + function normalizeHz(raw) { + if (!Number.isFinite(raw) || raw <= 0) + return TICKS_PER_SECOND; + let best = VALID_HZ[0]; + for (const v of VALID_HZ) { + if (Math.abs(v - raw) < Math.abs(best - raw)) + best = v; + } + return best; + } + function ticksPerFrame() { + return TICKS_PER_SECOND / hz; + } + function virtualFrame() { + return frame < 0 ? 0 : frame; + } + function resetClock() { + const raw = globalThis.__simHz; + hz = typeof raw === "number" ? normalizeHz(raw) : TICKS_PER_SECOND; + frame = -1; + timers = []; + timerSeq = 0; + } + function __advanceClock() { + frame = frame < 0 ? 0 : frame + 1; + if (timers.length === 0) + return; + const due = timers.filter((t) => t.at <= frame).sort((a, b) => a.at - b.at || a.seq - b.seq); + if (due.length === 0) + return; + timers = timers.filter((t) => t.at > frame); + for (const t of due) + t.cb(); + } + + // framework/src/analog.ts + var ANALOG_DEADZONE = 0.12; + var analogPacked = ANALOG_CENTER; + function __setAnalog(packed) { + analogPacked = packed === undefined ? ANALOG_CENTER : packed & 65535; + } + function __resetAnalog() { + analogPacked = ANALOG_CENTER; + } + function axis(raw) { + const value = Math.max(-1, Math.min(1, (raw - 128) / 127)); + const magnitude = Math.abs(value); + if (magnitude < ANALOG_DEADZONE) + return 0; + return Math.sign(value) * (magnitude - ANALOG_DEADZONE) / (1 - ANALOG_DEADZONE); + } + function analogX() { + return axis(analogPacked >> 8 & 255); + } + function analogY() { + return axis(analogPacked & 255); + } + + // framework/src/frame.ts + var callbacks = new Set; + var buttonHandlerBlockDepth = 0; + function resetFrameHooks() { + callbacks.clear(); + buttonHandlerBlockDepth = 0; + __resetAnalog(); + } + function runFrameHooks(buttons) { + for (const cb of [...callbacks]) + cb(buttons); + } + function onFrame(callback) { + callbacks.add(callback); + onCleanup(() => callbacks.delete(callback)); + } + function createSpriteAnimation(frames, opts = {}) { + if (frames.length === 0) { + throw new Error("PocketJS: createSpriteAnimation() requires at least one frame"); + } + const frameStep = Math.max(1, Math.floor(opts.frameStep ?? 1)); + const [frame2, setFrame] = createSignal(0); + onFrame(() => { + setFrame((frame2() + 1) % (frames.length * frameStep)); + }); + return () => frames[Math.floor(frame2() / frameStep) % frames.length]; + } + + // framework/src/pak.ts + var map = null; + var bytes = null; + function readKey(u8, off, len) { + let s = ""; + for (let i = 0;i < len; i++) + s += String.fromCharCode(u8[off + i]); + return s; + } + function parse(ab) { + const dv = new DataView(ab); + if (ab.byteLength < PAK_HEADER_SIZE || dv.getUint32(0, true) !== PAK_MAGIC) { + throw new Error("pak: bad magic"); + } + const version = dv.getUint16(4, true); + if (version !== PAK_VERSION) { + throw new Error("pak: unsupported version " + version); + } + const entryCount = dv.getUint32(8, true); + const dirOff = dv.getUint32(12, true); + const namesOff = dv.getUint32(16, true); + const u8 = new Uint8Array(ab); + const m = new Map; + for (let i = 0;i < entryCount; i++) { + const e = dirOff + i * PAK_ENTRY_SIZE; + const blobOff = dv.getUint32(e + 4, true); + const byteLen = dv.getUint32(e + 8, true); + const nameOff = dv.getUint32(e + 12, true); + const nameLen = dv.getUint16(e + 16, true); + const dtype = u8[e + 18]; + m.set(readKey(u8, namesOff + nameOff, nameLen), { + off: blobOff, + len: byteLen, + dtype + }); + } + map = m; + bytes = u8; + } + function loadPack(ab) { + parse(ab); + } + function ensureLoaded() { + if (map) + return; + const ab = globalThis.__pak; + if (!ab) + return; + parse(ab); + } + function hasPack() { + ensureLoaded(); + return map !== null; + } + function entries(prefix = "") { + ensureLoaded(); + if (!map) + return []; + const out = []; + for (const key of map.keys()) { + if (key.length >= prefix.length && key.slice(0, prefix.length) === prefix) { + out.push(key); + } + } + out.sort(); + return out; + } + function get(key) { + ensureLoaded(); + const e = map ? map.get(key) : undefined; + if (!e) { + throw new Error("pak: missing key " + key + " (no __pak provided, or the pack is incomplete)"); + } + return bytes.slice(e.off, e.off + e.len); + } + + // framework/src/input.ts + var root = null; + var focused = null; + var pressedNode = null; + var prevButtons = 0; + var focusScopeStack = []; + var focusGridStack = []; + var focusControllerStack = []; + function setInputRoot(r) { + root = r; + focused = null; + pressedNode = null; + prevButtons = 0; + focusScopeStack.length = 0; + focusGridStack.length = 0; + focusControllerStack.length = 0; + if (cursor) { + cursor.pressTarget = null; + cursor.target = null; + cursor.spriteDirty = true; + cursor.fresh = true; + cursor.vw = 0; + if (cursor.tex >= 0) { + const ops = getOps(); + ops.setCursor?.(-1, 0, 0, 0, 0); + ops.freeTexture?.(cursor.tex); + cursor.tex = -1; + } + } + } + function registerPress(node, fn) { + node.onPress = fn ?? undefined; + } + function registerFocusable(node, on) { + node.focusable = on; + __notifyTreeMutation(); + if (!on && focused === node) { + focusNode(null); + } + } + function focusNode(node) { + if (pressedNode && pressedNode !== node) + setPressedNode(null); + focused = node; + getOps().setFocus(node ? node.id : 0); + } + function setPressedNode(node) { + if (pressedNode === node) + return; + const ops = getOps(); + if (pressedNode) + ops.setActive?.(pressedNode.id, 0); + pressedNode = node; + if (node) + ops.setActive?.(node.id, 1); + } + function activeFocusRoot() { + return focusScopeStack.length > 0 ? focusScopeStack[focusScopeStack.length - 1] : root; + } + function collectFocusables(node, out) { + if (!node) + return; + if (node.focusable) + out.push(node); + if (!Array.isArray(node.children)) + return; + for (let i = 0;i < node.children.length; i++) { + collectFocusables(node.children[i], out); + } + } + function focusables() { + const out = []; + const r = activeFocusRoot(); + if (r) + collectFocusables(r, out); + return out; + } + function linearDirection(direction) { + return direction === "down" || direction === "right" ? 1 : -1; + } + function moveLinearFocus(direction) { + const dir = linearDirection(direction); + const list = focusables(); + if (list.length === 0) { + if (focused) + focusNode(null); + return; + } + const i = focused ? list.indexOf(focused) : -1; + if (i < 0) { + focusNode(dir === 1 ? list[0] : list[list.length - 1]); + return; + } + const j = i + dir; + if (j < 0 || j >= list.length) + return; + focusNode(list[j]); + } + function activeGrid() { + if (!focused) + return null; + const active = activeFocusRoot(); + if (active && !isWithin(focused, active)) + return null; + for (let i = focusGridStack.length - 1;i >= 0; i--) { + const grid = focusGridStack[i]; + if (active && !isWithin(grid.node, active) && !isWithin(active, grid.node)) + continue; + if (isWithin(focused, grid.node)) + return grid; + } + return null; + } + function moveGridFocus(direction) { + const grid = activeGrid(); + if (!grid) + return false; + const list = []; + collectFocusables(grid.node, list); + if (list.length === 0) { + if (focused) + focusNode(null); + return true; + } + const columns = grid.columns; + const i = focused ? list.indexOf(focused) : -1; + if (i < 0) { + focusNode(linearDirection(direction) === 1 ? list[0] : list[list.length - 1]); + return true; + } + let j = i; + switch (direction) { + case "right": + if (i + 1 < list.length && i % columns < columns - 1) + j = i + 1; + else if (grid.wrap) + j = Math.floor(i / columns) * columns; + break; + case "left": + if (i % columns > 0) + j = i - 1; + else if (grid.wrap) + j = Math.min(list.length - 1, Math.floor(i / columns) * columns + columns - 1); + break; + case "down": + if (i + columns < list.length) + j = i + columns; + else if (grid.wrap) + j = i % columns; + break; + case "up": + if (i - columns >= 0) + j = i - columns; + else if (grid.wrap) { + const col = i % columns; + j = col; + while (j + columns < list.length) + j += columns; + } + break; + } + if (j !== i) + focusNode(list[j]); + return true; + } + function activeController() { + if (!focused) + return null; + const active = activeFocusRoot(); + if (active && !isWithin(focused, active)) + return null; + for (let i = focusControllerStack.length - 1;i >= 0; i--) { + const ctl = focusControllerStack[i]; + if (active && !isWithin(ctl.node, active) && !isWithin(active, ctl.node)) + continue; + if (isWithin(focused, ctl.node)) + return ctl; + } + return null; + } + function moveFocus(direction) { + const ctl = activeController(); + if (ctl && ctl.move(direction)) + return; + if (moveGridFocus(direction)) + return; + moveLinearFocus(direction); + } + function firePress() { + let n = focused; + while (n) { + if (n.onPress) { + n.onPress(); + return; + } + n = n.parent; + } + } + function isWithin(node, ancestor) { + if (!node || !ancestor) + return false; + let n = node; + while (n) { + if (n === ancestor) + return true; + n = n.parent; + } + return false; + } + function firstFocusable(node) { + if (!node) + return null; + if (node.focusable) + return node; + if (!Array.isArray(node.children)) + return null; + for (let i = 0;i < node.children.length; i++) { + const f = firstFocusable(node.children[i]); + if (f) + return f; + } + return null; + } + function notifyDetached(node) { + if (!focused || !isWithin(focused, node)) + return; + const parent = node.parent; + if (parent) { + const idx = parent.children.indexOf(node); + for (let i = idx + 1;i < parent.children.length; i++) { + const f = firstFocusable(parent.children[i]); + if (f) { + focusNode(f); + return; + } + } + for (let i = idx - 1;i >= 0; i--) { + const f = firstFocusable(parent.children[i]); + if (f) { + focusNode(f); + return; + } + } + let a = parent; + while (a) { + if (a.focusable) { + focusNode(a); + return; + } + a = a.parent; + } + } + focusNode(null); + } + var cursor = null; + var inputGen = 0; + function __notifyTreeMutation() { + inputGen++; + } + var ARROW_OUTLINE = [1, 3, 5, 9, 17, 33, 65, 129, 257, 513, 1985, 73, 149, 147, 288, 480]; + var ARROW_FILL = [0, 0, 2, 6, 14, 30, 62, 126, 254, 510, 62, 54, 98, 96, 192, 0]; + function defaultArrowRGBA() { + const px = new Uint8Array(16 * 16 * 4); + for (let y = 0;y < 16; y++) { + for (let x = 0;x < 16; x++) { + const outline = ARROW_OUTLINE[y] >> x & 1; + const fill = ARROW_FILL[y] >> x & 1; + if (!outline && !fill) + continue; + const i = (y * 16 + x) * 4; + const v = fill ? 255 : 0; + px[i] = v; + px[i + 1] = v; + px[i + 2] = v; + px[i + 3] = 255; + } + } + return px; + } + function cursorInitSprite(c, ops) { + const sprite = c.sprite; + c.spriteDirty = false; + const old = c.tex; + let tex = -1; + let blob = null; + if (typeof sprite.image === "string") { + try { + blob = get(sprite.image); + } catch (err) { + if (getHost().strict) + throw err; + blob = null; + } + } else if (sprite.image) { + blob = sprite.image; + } + if (blob) { + tex = ops.uploadImgEntry ? ops.uploadImgEntry(blob) : uploadImgFallback(ops, blob); + if (tex < 0 && getHost().strict) { + throw new Error("enableCursor: cursor image rejected (malformed or RLE-only IMG entry)"); + } + } + if (tex < 0) { + tex = ops.uploadTexture(defaultArrowRGBA(), 16, 16, PSM.PSM_8888); + } + c.tex = tex; + ops.setCursor(tex, sprite.hotspot[0], sprite.hotspot[1], sprite.size[0], sprite.size[1]); + if (old >= 0 && old !== tex) + ops.freeTexture?.(old); + } + function uploadImgFallback(ops, blob) { + if (blob.length < 8) + return -1; + const dv = new DataView(blob.buffer, blob.byteOffset, blob.byteLength); + if (blob[5] & IMG_FLAG_RLE) + return -1; + return ops.uploadTexture(blob.subarray(8), dv.getUint16(0, true), dv.getUint16(2, true), blob[4]); + } + function findMirror(node, id) { + if (!node || id === 0) + return null; + if (node.id === id) + return node; + const kids = node.children; + if (!Array.isArray(kids)) + return null; + for (let i = 0;i < kids.length; i++) { + const found = findMirror(kids[i], id); + if (found) + return found; + } + return null; + } + var hitRoot = null; + function setHitRoot(r) { + hitRoot = r; + } + function cursorTarget(hit) { + const scope = focusScopeStack.length > 0 ? focusScopeStack[focusScopeStack.length - 1] : null; + let n = hit; + while (n) { + if (n.focusable && (!scope || isWithin(n, scope))) + return n; + n = n.parent; + } + return null; + } + function cursorFrame(buttons, pressed, released) { + const c = cursor; + const ops = getOps(); + if (!ops.hitTest || !ops.setCursor || !ops.setCursorPos) + return false; + if (c.vw === 0) { + const vp = hostViewport(ops); + c.vw = vp ? vp.w : SCREEN_W; + c.vh = vp ? vp.h : SCREEN_H; + if (c.x < 0) { + c.x = Math.floor(c.vw / 2); + c.y = Math.floor(c.vh / 2); + } + } + if (c.spriteDirty) + cursorInitSprite(c, ops); + let vx = analogX() * c.speed; + let vy = analogY() * c.speed; + if (c.dpadSpeed > 0 && vx === 0 && vy === 0) { + if (buttons & BTN.LEFT) + vx = -c.dpadSpeed; + if (buttons & BTN.RIGHT) + vx = c.dpadSpeed; + if (buttons & BTN.UP) + vy = -c.dpadSpeed; + if (buttons & BTN.DOWN) + vy = c.dpadSpeed; + } + let moved = c.fresh; + if (vx !== 0 || vy !== 0) { + const dt = ticksPerFrame() / 60; + const nx = Math.min(Math.max(c.x + vx * dt, 0), c.vw - 1); + const ny = Math.min(Math.max(c.y + vy * dt, 0), c.vh - 1); + if (nx !== c.x || ny !== c.y) { + c.x = nx; + c.y = ny; + moved = true; + } + } + if (moved) + ops.setCursorPos(c.x, c.y); + const edges = (pressed | released) & c.button; + const gen = inputGen; + if (moved || edges !== 0 || gen !== c.gen) { + c.gen = gen; + c.fresh = false; + c.target = cursorTarget(findMirror(hitRoot ?? root, ops.hitTest(c.x, c.y))); + } + const target = c.target; + if (target !== focused) + focusNode(target); + if (pressed & c.button && target) { + c.pressTarget = target; + } + if (c.pressTarget) { + setPressedNode(target === c.pressTarget ? c.pressTarget : null); + if (released & c.button) { + const fire = target === c.pressTarget; + c.pressTarget = null; + setPressedNode(null); + if (fire) + firePress(); + } + } else if (released & c.button) { + setPressedNode(null); + } + return true; + } + function handleFrame(buttons) { + const pressed = buttons & ~prevButtons; + const released = prevButtons & ~buttons; + prevButtons = buttons; + if (cursor && cursorFrame(buttons, pressed, released)) + return; + if (released & BTN.CIRCLE) + setPressedNode(null); + if (pressed === 0) + return; + if (pressed & BTN.DOWN) + moveFocus("down"); + if (pressed & BTN.RIGHT) + moveFocus("right"); + if (pressed & BTN.UP) + moveFocus("up"); + if (pressed & BTN.LEFT) + moveFocus("left"); + if (pressed & BTN.CIRCLE) { + setPressedNode(focused); + firePress(); + } + } + + // framework/src/native-tree.ts + var treeMutationHook = null; + function setTreeMutationHook(fn) { + treeMutationHook = fn; + } + function treeMutated() { + __notifyTreeMutation(); + if (treeMutationHook) + treeMutationHook(); + } + function setDebugName(node, name) { + node.debugName = name || undefined; + treeMutated(); + } + var rootMirror = { + id: ROOT_ID, + type: NODE_TYPE.view, + parent: null, + children: [], + domNodeType: 1, + domTag: "root" + }; + var DOM_NODE = Symbol.for("pocketjs.native-node"); + var DOM_ELEMENT = 1; + var DOM_TEXT = 3; + var DOM_COMMENT = 8; + var NATIVE_ATTRIBUTE_NAMES = new Set(["class", "className", "style", "src", "onPress", "on:press", "focusable", "debugName", "ref", "nodeRef", "key", "children"]); + function domAttrs(node) { + return node.domAttrs ??= {}; + } + function cloneNativeNode(node, deep) { + const nodeType = node.domNodeType ?? (isTextNode(node) ? DOM_TEXT : DOM_ELEMENT); + const clone = nodeType === DOM_TEXT ? createTextNode(node.text ?? "") : nodeType === DOM_COMMENT ? createCommentNode(node.domData ?? "") : createElement(node.domTag ?? tagName(node)); + for (const key of Object.keys(node.domAttrs ?? {})) { + setDomAttribute(clone, key, node.domAttrs[key]); + } + if (deep) { + for (const child of node.children) + insertNode(clone, cloneNativeNode(child, true)); + } + return clone; + } + function setDomAttribute(node, name, value) { + if (NATIVE_ATTRIBUTE_NAMES.has(name)) { + setProp(node, name, value, node.domAttrs?.[name]); + return; + } + if (value == null) + delete domAttrs(node)[name]; + else + domAttrs(node)[name] = value; + } + function decorateNativeNode(node) { + if (node[DOM_NODE] === true) + return node; + Object.defineProperty(node, DOM_NODE, { + value: true + }); + Object.defineProperties(node, { + nodeType: { + configurable: true, + get() { + return node.domNodeType ?? (isTextNode(node) ? DOM_TEXT : DOM_ELEMENT); + } + }, + nodeValue: { + configurable: true, + get() { + return node.domNodeType === DOM_COMMENT ? node.domData ?? "" : node.text ?? ""; + }, + set(value) { + if (node.domNodeType === DOM_COMMENT) + node.domData = String(value ?? ""); + else + replaceText(node, String(value ?? "")); + } + }, + data: { + configurable: true, + get() { + return node.domNodeType === DOM_COMMENT ? node.domData ?? "" : node.text ?? ""; + }, + set(value) { + if (node.domNodeType === DOM_COMMENT) + node.domData = String(value ?? ""); + else + replaceText(node, String(value ?? "")); + } + }, + textContent: { + configurable: true, + get() { + if (node.domNodeType === DOM_COMMENT) + return node.domData ?? ""; + if (isTextNode(node)) + return node.text ?? ""; + return node.children.map((child) => child.text ?? "").join(""); + }, + set(value) { + const text = String(value ?? ""); + if (node.domNodeType === DOM_COMMENT) { + node.domData = text; + } else if (isTextNode(node)) { + replaceText(node, text); + } else { + clearContainer(node); + if (text) + insertNode(node, createTextNode(text)); + } + } + }, + parentNode: { + configurable: true, + get() { + return node.parent; + } + }, + parentElement: { + configurable: true, + get() { + return node.parent; + } + }, + childNodes: { + configurable: true, + get() { + return node.children; + } + }, + firstChild: { + configurable: true, + get() { + return node.children[0] ?? null; + } + }, + lastChild: { + configurable: true, + get() { + return node.children[node.children.length - 1] ?? null; + } + }, + nextSibling: { + configurable: true, + get() { + return getNextSibling(node) ?? null; + } + }, + previousSibling: { + configurable: true, + get() { + const parent = node.parent; + if (!parent) + return null; + const index = parent.children.indexOf(node); + return index > 0 ? parent.children[index - 1] : null; + } + }, + tagName: { + configurable: true, + get() { + return (node.domTag ?? tagName(node)).toUpperCase(); + } + }, + nodeName: { + configurable: true, + get() { + if (node.domNodeType === DOM_TEXT) + return "#text"; + if (node.domNodeType === DOM_COMMENT) + return "#comment"; + return (node.domTag ?? tagName(node)).toUpperCase(); + } + }, + className: { + configurable: true, + get() { + return String(node.domAttrs?.class ?? ""); + }, + set(value) { + setProp(node, "class", value, node.domAttrs?.class); + } + }, + isConnected: { + configurable: true, + get() { + let current2 = node; + while (current2) { + if (current2 === rootMirror) + return true; + current2 = current2.parent; + } + return false; + } + } + }); + const methods = { + appendChild(child) { + insertNode(node, child); + return child; + }, + insertBefore(child, anchor) { + insertNode(node, child, anchor ?? null); + return child; + }, + removeChild(child) { + removeNode(node, child); + return child; + }, + replaceChild(next, current2) { + insertNode(node, next, current2); + removeNode(node, current2); + return current2; + }, + cloneNode(deep = false) { + return cloneNativeNode(node, !!deep); + }, + remove() { + if (node.parent) + removeNode(node.parent, node); + }, + setAttribute(name, value) { + setDomAttribute(node, name, value); + }, + removeAttribute(name) { + setDomAttribute(node, name, undefined); + }, + getAttribute(name) { + const value = node.domAttrs?.[name]; + return value == null ? null : String(value); + }, + hasAttribute(name) { + return node.domAttrs?.[name] != null; + }, + hasChildNodes() { + return node.children.length > 0; + }, + contains(other) { + let current2 = other ?? null; + while (current2) { + if (current2 === node) + return true; + current2 = current2.parent; + } + return false; + }, + addEventListener() {}, + removeEventListener() {} + }; + Object.assign(node, methods, { + style: { + length: 0, + item: () => "" + }, + classList: { + add() {}, + remove() {} + } + }); + return node; + } + decorateNativeNode(rootMirror); + var styleResolver = null; + function setStyleResolver(fn) { + styleResolver = fn; + } + var missCounters = { + unknownClass: 0, + unknownTexture: 0 + }; + var textures = new Map; + function registerTexture(key, handle) { + textures.set(key, handle); + } + var sprites = new Map; + function registerSprite(key, meta) { + sprites.set(key, meta); + } + var sweepSet = new Set; + var retained = new Set; + function subtreeHasRetained(node) { + if (!node) + return false; + if (retained.has(node)) + return true; + if (node.children) { + for (let i = 0;i < node.children.length; i++) { + if (subtreeHasRetained(node.children[i])) + return true; + } + } + return false; + } + function runSweep() { + if (sweepSet.size === 0) + return; + const ops = getOps(); + const keep = []; + for (const node of sweepSet) { + if (!node) + continue; + if (node.parent !== null) + continue; + if (subtreeHasRetained(node)) { + keep.push(node); + continue; + } + ops.destroyNode(node.id); + } + sweepSet.clear(); + for (let i = 0;i < keep.length; i++) + sweepSet.add(keep[i]); + } + function createElement(tag) { + const type = NODE_TYPE[tag]; + if (type === undefined) { + throw new Error(`PocketJS: unknown element <${tag}> - only view/text/image exist`); + } + return decorateNativeNode({ + id: getOps().createNode(type), + type, + parent: null, + children: [], + domNodeType: DOM_ELEMENT, + domTag: tag + }); + } + function createTextNode(value) { + const ops = getOps(); + const id = ops.createNode(NODE_TYPE.text); + ops.setText(id, value); + return decorateNativeNode({ + id, + type: NODE_TYPE.text, + parent: null, + children: [], + text: value, + domNodeType: DOM_TEXT, + domTag: "#text" + }); + } + function createCommentNode(data = "") { + const node = createTextNode(""); + node.domNodeType = DOM_COMMENT; + node.domTag = "#comment"; + node.domData = data; + return node; + } + function replaceText(node, value) { + getOps().replaceText(node.id, value); + node.text = value; + treeMutated(); + } + function isTextNode(node) { + return node.type === NODE_TYPE.text; + } + function unlink(node) { + const p = node.parent; + if (!p) + return; + const i = p.children.indexOf(node); + if (i >= 0) + p.children.splice(i, 1); + node.parent = null; + } + function insertNode(parent, node, anchor) { + const ops = getOps(); + unlink(node); + sweepSet.delete(node); + ops.insertBefore(parent.id, node.id, anchor ? anchor.id : 0); + if (anchor) { + const i = parent.children.indexOf(anchor); + if (i < 0) + throw new Error("PocketJS: insert anchor is not a child of parent"); + parent.children.splice(i, 0, node); + } else { + parent.children.push(node); + } + node.parent = parent; + treeMutated(); + } + function removeNode(parent, node) { + if (!node) + return; + notifyDetached(node); + getOps().removeChild(parent.id, node.id); + unlink(node); + sweepSet.add(node); + treeMutated(); + } + function getParentNode(node) { + return node.parent ?? undefined; + } + function getFirstChild(node) { + return node.children[0]; + } + function getNextSibling(node) { + const p = node.parent; + if (!p) + return; + const i = p.children.indexOf(node); + return i >= 0 ? p.children[i + 1] : undefined; + } + function setClass(node, value) { + const ops = getOps(); + treeMutated(); + if (value == null || value === "") { + ops.setStyle(node.id, STYLE_ID_NONE); + return; + } + if (typeof value !== "string") { + throw new Error("PocketJS: class must be a string literal of utilities"); + } + const styleId = styleResolver ? styleResolver(value) : undefined; + if (styleId === undefined) { + if (getHost().strict) { + throw new Error(`PocketJS: unknown class "${value}" - not in the compiled style table ` + "(dynamic classes must be ternaries of full literals)"); + } + missCounters.unknownClass++; + return; + } + ops.setStyle(node.id, styleId); + } + function setSrc(node, value) { + const ops = getOps(); + if (value == null || value === "") { + ops.setImage(node.id, -1); + return; + } + if (typeof value !== "string") { + throw new Error("PocketJS: src must be a string key"); + } + const handle = textures.get(value); + if (handle === undefined) { + if (getHost().strict) { + throw new Error(`PocketJS: unknown image src "${value}" - no texture registered under that key`); + } + missCounters.unknownTexture++; + return; + } + ops.setImage(node.id, handle); + } + function setSpriteSrc(node, value) { + const ops = getOps(); + if (value == null || value === "") { + ops.setSprite(node.id, -1, 0, 0, 0); + return; + } + if (typeof value !== "string") { + throw new Error("PocketJS: sprite must be a string key"); + } + const meta = sprites.get(value); + if (meta === undefined) { + if (getHost().strict) { + throw new Error(`PocketJS: unknown sprite "${value}" - no sprite atlas registered under that key`); + } + missCounters.unknownTexture++; + return; + } + ops.setSprite(node.id, meta.handle, meta.frames, meta.cols, meta.step); + } + function setStyleObject(node, value, prev) { + const ops = getOps(); + const next = value ?? {}; + const before = prev ?? {}; + let changed = false; + for (const key in next) { + const v = next[key]; + if (before[key] === v) + continue; + const propId = PROP[key]; + if (propId === undefined) { + throw new Error(`PocketJS: unknown style prop '${key}' (see spec PROP)`); + } + ops.setProp(node.id, propId, encodePropValue(key, v)); + changed = true; + } + if (changed) + treeMutated(); + } + function setProp(node, name, value, prev) { + if (value === prev && name !== "style") + return value; + if (name === "className") + name = "class"; + if (name !== "children" && name !== "key" && name !== "ref" && name !== "nodeRef") { + if (value == null) + delete domAttrs(node)[name]; + else + domAttrs(node)[name] = value; + } + switch (name) { + case "class": + setClass(node, value); + return value; + case "onPress": + case "on:press": + registerPress(node, value); + return value; + case "src": + setSrc(node, value); + return value; + case "sprite": + setSpriteSrc(node, value); + return value; + case "style": + setStyleObject(node, value, prev); + return value; + case "focusable": + registerFocusable(node, !!value); + return value; + case "debugName": + setDebugName(node, value == null ? undefined : String(value)); + return value; + case "ref": + case "nodeRef": + case "key": + case "children": + return value; + default: + break; + } + if (name === "classList") { + throw new Error("PocketJS: classList is not supported - use ternaries of full class literals"); + } + if (name.startsWith("on:") || name.startsWith("bool:") || name.startsWith("prop:")) { + throw new Error(`PocketJS: unsupported namespaced attribute '${name}'`); + } + throw new Error(`PocketJS: unknown property '${name}' on <${tagName(node)}>`); + } + function clearContainer(container) { + for (const child of [...container.children]) + removeNode(container, child); + } + function tagName(node) { + for (const key of Object.keys(NODE_TYPE)) { + if (NODE_TYPE[key] === node.type) + return key; + } + return String(node.type); + } + + // framework/src/renderer-solid.ts + function setProperty(node, name, value, prev) { + if (name === "ref" && typeof value === "function") { + value(node); + return; + } + setProp(node, name, value, prev); + } + var renderer = createRenderer({ + createElement, + createTextNode, + replaceText, + isTextNode, + setProperty, + insertNode(parent, node, anchor) { + insertNode(parent, node, anchor); + }, + removeNode(parent, node) { + removeNode(parent, node); + }, + getParentNode, + getFirstChild, + getNextSibling + }); + var { + render, + effect, + memo: memo2, + createComponent: createComponent2, + createElement: createElement2, + insert, + spread, + mergeProps: mergeProps2, + use + } = renderer; + + // framework/src/anim.ts + var EASING_BY_NAME = { + linear: ENUMS.Easing.Linear, + in: ENUMS.Easing.EaseIn, + out: ENUMS.Easing.EaseOut, + "in-out": ENUMS.Easing.EaseInOut, + "out-back": ENUMS.Easing.OutBack, + spring: ENUMS.Easing.Spring, + "spring-bouncy": ENUMS.Easing.SpringBouncy + }; + function nodeId(node) { + return typeof node === "number" ? node : node.id; + } + function animatablePropId(prop) { + const propId = PROP[prop]; + if (propId === undefined) { + throw new Error(`PocketJS: unknown prop '${prop}'`); + } + if (animBit(prop) < 0) { + throw new Error(`PocketJS: prop '${prop}' is not animatable (see spec ANIMATABLE)`); + } + return propId; + } + function animate(node, prop, to, opts = {}) { + const propId = animatablePropId(prop); + let easing; + if (typeof opts.easing === "number") { + easing = opts.easing; + } else { + const named = EASING_BY_NAME[opts.easing ?? "out"]; + if (named === undefined) { + throw new Error(`PocketJS: unknown easing '${opts.easing}'`); + } + easing = named; + } + return getOps().animate(nodeId(node), propId, encodePropValue(prop, to), opts.dur ?? 200, easing, opts.delay ?? 0); + } + + // framework/src/overlay.ts + var overlayRoot = null; + function setOverlayRoot(root2) { + overlayRoot = root2; + } + // framework/src/primitives.ts + function callRef(ref, node) { + if (!ref) + return; + if (typeof ref === "function") + ref(node); + else if ("current" in ref) + ref.current = node; + } + function primitive(tag, props) { + const el = createElement2(tag); + spread(el, props, false); + callRef(props.nodeRef, el); + return el; + } + function View(props) { + return primitive("view", props); + } + function Text(props) { + return primitive("text", props); + } + function Image(props) { + return primitive("image", props); + } + // framework/src/hot.ts + var lastText = new WeakMap; + var lastProp = new WeakMap; + + // framework/src/platform.ts + var features = {} !== null ? Object.freeze({ + ...{} + }) : Object.freeze({}); + var platform = Object.freeze({ + target: "", + pixelRatio: Number.isInteger(1) ? 1 : 1, + features + }); + + // framework/src/tiles.ts + var parsed = new Map; + // framework/src/devtools.ts + var TAPE_CAP = 36000; + var TREE_THROTTLE = 30; + var STATS_EVERY = 30; + var state = { + ops: null, + transport: null, + app: undefined, + frame: 0, + tape: new Uint16Array(TAPE_CAP), + tapeAnalog: new Uint16Array(TAPE_CAP), + tapeStart: 0, + tapeLen: 0, + tapeFirstFrame: 0, + replayMasks: null, + replayAnalog: null, + replayAt: 0, + paused: false, + stepQueued: 0, + inspectReportId: null, + inspectAskedAt: 0, + treeDirty: true, + treeSentAt: -TREE_THROTTLE, + saidHello: false, + hostCalls: 0 + }; + function initDevtools(ops) { + const g = globalThis; + if (!g.console) + g.console = { + log() {}, + warn() {}, + error() {} + }; + state.ops = ops; + state.frame = 0; + state.tapeStart = 0; + state.tapeLen = 0; + state.tapeFirstFrame = 0; + state.replayMasks = null; + state.replayAnalog = null; + state.paused = false; + state.stepQueued = 0; + state.inspectReportId = null; + state.inspectAskedAt = 0; + state.treeDirty = true; + state.treeSentAt = -TREE_THROTTLE; + state.saidHello = false; + state.hostCalls = 0; + state.app = globalThis.__pocketApp; + const injected = globalThis.__pocketDevtoolsTransport; + if (injected) { + state.transport = injected; + } else if (ops.__dbgActive?.() && ops.__dbgPoll && ops.__dbgSend) { + state.transport = { + send: (l) => ops.__dbgSend(l), + recv: () => ops.__dbgPoll(), + everyFrames: 10 + }; + } else { + state.transport = null; + } + if (state.transport) { + setTreeMutationHook(() => { + state.treeDirty = true; + }); + bridgeConsole(); + } else { + setTreeMutationHook(null); + } + globalThis.__pocketDevtools = api; + } + function wrapFrameHandler(h) { + return (buttons, analogArg, touchArg) => { + state.hostCalls++; + if (state.transport) { + pollTransport(); + flushInspectReport(); + } + let mask = buttons; + let analog = analogArg === undefined ? ANALOG_CENTER : analogArg & 65535; + let touch = touchArg; + if (state.replayMasks) { + if (state.replayAt < state.replayMasks.length) { + mask = state.replayMasks[state.replayAt]; + analog = state.replayAnalog ? state.replayAnalog[state.replayAt] : ANALOG_CENTER; + touch = undefined; + state.replayAt++; + } else { + state.replayMasks = null; + state.replayAnalog = null; + send({ + t: "replayDone", + frame: state.frame + }); + } + } + if (state.paused) { + if (state.stepQueued <= 0) + return; + state.stepQueued--; + state.ops?.debugStep?.(); + } + recordMask(mask, analog); + state.frame++; + try { + h(mask, analog, touch); + } catch (e) { + send({ + t: "error", + frame: state.frame, + message: e instanceof Error ? e.message : String(e), + stack: e instanceof Error ? e.stack : undefined + }); + throw e; + } + if (state.transport) + afterFrame(); + }; + } + function recordMask(mask, analog) { + if (state.tapeLen < TAPE_CAP) { + const at = (state.tapeStart + state.tapeLen) % TAPE_CAP; + state.tape[at] = mask; + state.tapeAnalog[at] = analog; + state.tapeLen++; + } else { + state.tape[state.tapeStart] = mask; + state.tapeAnalog[state.tapeStart] = analog; + state.tapeStart = (state.tapeStart + 1) % TAPE_CAP; + state.tapeFirstFrame++; + } + } + function rlePairs(ring) { + const out = []; + for (let i = 0;i < state.tapeLen; i++) { + const v = ring[(state.tapeStart + i) % TAPE_CAP]; + const last = out[out.length - 1]; + if (last && last[0] === v) + last[1]++; + else + out.push([v, 1]); + } + return out; + } + function exportTape() { + const tape = { + v: 1, + app: state.app, + frames: state.tapeLen, + masks: rlePairs(state.tape), + startFrame: state.tapeFirstFrame + }; + const analog = rlePairs(state.tapeAnalog); + if (analog.length > 1 || analog.length === 1 && analog[0][0] !== ANALOG_CENTER) { + tape.analog = analog; + } + return tape; + } + function expandPairs(pairs, fill, total) { + const out = new Uint16Array(total).fill(fill); + let at = 0; + for (const [v, n] of pairs) { + out.fill(v, at, Math.min(at + n, total)); + at += n; + } + return out; + } + function expandTape(tape) { + let total = 0; + for (const [, n] of tape.masks) + total += n; + return expandPairs(tape.masks, 0, total); + } + function expandTapeAnalog(tape) { + let total = 0; + for (const [, n] of tape.masks) + total += n; + return expandPairs(tape.analog ?? [], ANALOG_CENTER, total); + } + function send(msg) { + try { + state.transport?.send(JSON.stringify(msg)); + } catch {} + } + function pollTransport() { + const t = state.transport; + const every = t.everyFrames ?? 1; + if (every > 1 && state.hostCalls % every !== 0) + return; + if (!state.saidHello) { + state.saidHello = true; + send({ + t: "hello", + app: state.app, + host: hostKind(), + frame: state.frame + }); + } + for (let guard = 0;guard < 64; guard++) { + const chunk = t.recv(); + if (!chunk) + break; + for (const line of chunk.split(` +`)) { + if (line.trim()) + handleMessage(line); + } + } + } + function handleMessage(line) { + let msg; + try { + msg = JSON.parse(line); + } catch { + return; + } + const ops = state.ops; + switch (msg.t) { + case "inspect": { + const id = typeof msg.id === "number" ? msg.id : 0; + ops?.debugInspect?.(id); + state.inspectReportId = id || null; + state.inspectAskedAt = state.hostCalls; + if (!id) + send({ + t: "inspect", + id: 0, + rect: null + }); + break; + } + case "pause": + state.paused = true; + state.stepQueued = 0; + ops?.debugPause?.(true); + sendStats(); + break; + case "resume": + state.paused = false; + ops?.debugPause?.(false); + sendStats(); + break; + case "step": + state.stepQueued += typeof msg.n === "number" && msg.n > 0 ? msg.n : 1; + break; + case "getTree": + sendTree(); + break; + case "eval": { + let ok = true; + let value; + try { + value = fmt((0, eval)(String(msg.code))); + } catch (e) { + ok = false; + value = e instanceof Error ? `${e.name}: ${e.message}` : String(e); + } + send({ + t: "evalResult", + id: msg.id, + ok, + value + }); + break; + } + case "dumpTape": + send({ + t: "tape", + tape: exportTape() + }); + break; + case "devStats": { + let data = null; + const raw = ops?.debugStats?.(); + if (raw) { + try { + data = JSON.parse(raw); + } catch { + data = null; + } + } + send({ + t: "devStats", + frame: state.frame, + data + }); + break; + } + case "screenshot": { + if (ops?.__dbgShot?.()) { + send({ + t: "screenshotRaw", + file: "shot.raw", + w: 480, + h: 272, + stride: 512, + frame: state.frame + }); + } else { + send({ + t: "log", + level: "warn", + args: ["screenshot: not supported on this host"] + }); + } + break; + } + case "replay": { + const tape = msg.tape; + if (tape && Array.isArray(tape.masks)) { + state.replayMasks = expandTape(tape); + state.replayAnalog = tape.analog ? expandTapeAnalog(tape) : null; + state.replayAt = 0; + } + break; + } + default: + break; + } + } + function afterFrame() { + if (state.treeDirty && state.frame - state.treeSentAt >= TREE_THROTTLE) { + sendTree(); + } + if (state.frame % STATS_EVERY === 0) + sendStats(); + } + function flushInspectReport() { + const id = state.inspectReportId; + if (id == null) + return; + const ops = state.ops; + if (!ops?.debugRectXY || !ops.debugRectWH) { + state.inspectReportId = null; + return; + } + const xy = ops.debugRectXY(); + if (xy === -1) { + if (state.hostCalls - state.inspectAskedAt > 60) { + state.inspectReportId = null; + send({ + t: "inspect", + id, + rect: null + }); + } + return; + } + const wh = ops.debugRectWH(); + state.inspectReportId = null; + send({ + t: "inspect", + id, + rect: [xy << 16 >> 16, xy >> 16, wh & 65535, wh >> 16 & 65535] + }); + } + function sendStats() { + send({ + t: "stats", + frame: state.frame, + nodes: countNodes(rootMirror), + tapeLen: state.tapeLen, + paused: state.paused + }); + } + function sendTree() { + state.treeDirty = false; + state.treeSentAt = state.frame; + send({ + t: "tree", + frame: state.frame, + root: serializeNode(rootMirror) + }); + } + function isTreeMirror(value) { + if (value == null || typeof value !== "object") + return false; + const candidate = value; + return typeof candidate.id === "number" && typeof candidate.type === "number"; + } + function forEachTreeMirror(value, visit) { + if (Array.isArray(value)) { + for (const entry of value) + forEachTreeMirror(entry, visit); + return; + } + if (isTreeMirror(value)) { + visit(value); + return; + } + if (value != null && typeof value === "object") { + const nodes = value.nodes; + if (nodes !== undefined) + forEachTreeMirror(nodes, visit); + } + } + function forEachTreeChild(node, visit) { + const children2 = Array.isArray(node.children) ? node.children : []; + for (const child of children2) + forEachTreeMirror(child, visit); + } + function serializeNode(node) { + const out = { + i: node.id, + t: node.domTag ?? String(node.type) + }; + if (node.debugName) + out.n = node.debugName; + const cls = node.domAttrs?.class; + if (typeof cls === "string" && cls) + out.c = cls; + if (node.text) + out.x = node.text.length > 80 ? node.text.slice(0, 79) + "…" : node.text; + const kids = []; + forEachTreeChild(node, (child) => { + if (child.domNodeType === 8) + return; + kids.push(serializeNode(child)); + }); + if (kids.length) + out.k = kids; + return out; + } + function countNodes(node) { + let n = 1; + forEachTreeChild(node, (child) => { + n += countNodes(child); + }); + return n; + } + function bridgeConsole() { + const g = globalThis; + if (!g.console) + g.console = {}; + const c = g.console; + if (c.__pocketBridged) + return; + c.__pocketBridged = true; + for (const level of ["log", "warn", "error"]) { + const original = c[level]; + c[level] = (...args) => { + send({ + t: "log", + level, + args: args.map((a) => fmt(a)) + }); + original?.apply(c, args); + }; + } + } + function fmt(v, depth = 0) { + if (v === undefined) + return "undefined"; + if (v === null) + return "null"; + const t = typeof v; + if (t === "string") { + const s = v; + return depth === 0 ? clip(s) : JSON.stringify(clip(s)); + } + if (t === "number" || t === "boolean" || t === "bigint") + return String(v); + if (t === "function") { + const name = v.name; + return name ? `[function ${name}]` : "[function]"; + } + if (depth >= 3) + return Array.isArray(v) ? "[…]" : "{…}"; + if (Array.isArray(v)) { + const items = v.slice(0, 20).map((x) => fmt(x, depth + 1)); + if (v.length > 20) + items.push(`… ${v.length - 20} more`); + return `[${items.join(", ")}]`; + } + if (v instanceof Error) + return `${v.name}: ${v.message}`; + const entries2 = Object.entries(v).slice(0, 20); + const body = entries2.map(([k, x]) => `${k}: ${fmt(x, depth + 1)}`).join(", "); + return `{${body}}`; + } + function clip(s) { + return s.length > 200 ? s.slice(0, 199) + "…" : s; + } + function hostKind() { + const ops = state.ops; + if (typeof ops?.__host === "string") + return ops.__host; + if (ops?.__textures !== undefined) + return "psp"; + if (typeof globalThis.document !== "undefined") + return "web"; + return "headless"; + } + var api = { + get frame() { + return state.frame; + }, + dumpTape: () => exportTape(), + replay: (tape) => { + state.replayMasks = expandTape(tape); + state.replayAnalog = tape.analog ? expandTapeAnalog(tape) : null; + state.replayAt = 0; + } + }; + + // framework/src/styles.ts + var verbatim = new Map; + var sortedAlias = new Map; + function normalize(cls) { + return cls.trim().replace(/\s+/g, " "); + } + function sortTokens(normalized) { + return normalized.split(" ").sort().join(" "); + } + var ALIAS_AMBIGUOUS = -1; + function registerStyles(table) { + for (const key of Object.keys(table)) { + const id = table[key]; + const norm = normalize(key); + verbatim.set(norm, id); + const sorted = sortTokens(norm); + const prev = sortedAlias.get(sorted); + sortedAlias.set(sorted, prev !== undefined && prev !== id ? ALIAS_AMBIGUOUS : id); + } + } + function resolveStyle(cls) { + const norm = normalize(cls); + const hit = verbatim.get(norm); + if (hit !== undefined) + return hit; + const alias = sortedAlias.get(sortTokens(norm)); + return alias === ALIAS_AMBIGUOUS ? undefined : alias; + } + + // framework/src/touch.ts + var LEGACY_COORD_BITS = 9; + var LEGACY_COORD_MASK = (1 << LEGACY_COORD_BITS) - 1; + var LEGACY_ID_SHIFT = LEGACY_COORD_BITS * 2; + var WIDE_MARKER = 2147483648; + var WIDE_COORD_BITS = 10; + var WIDE_COORD_MASK = (1 << WIDE_COORD_BITS) - 1; + var WIDE_ID_SHIFT = WIDE_COORD_BITS * 2; + var EMPTY = Object.freeze([]); + var snapshot = EMPTY; + function __setTouches(packed) { + if (!packed || packed.length === 0) { + snapshot = EMPTY; + return; + } + snapshot = Object.freeze(packed.slice(0, 8).map((value) => { + const wide = (value & WIDE_MARKER) !== 0; + const coordBits = wide ? WIDE_COORD_BITS : LEGACY_COORD_BITS; + const coordMask = wide ? WIDE_COORD_MASK : LEGACY_COORD_MASK; + const idShift = wide ? WIDE_ID_SHIFT : LEGACY_ID_SHIFT; + return Object.freeze({ + id: value >>> idShift & 255, + x: value & coordMask, + y: value >>> coordBits & coordMask + }); + })); + } + function __resetTouches() { + snapshot = EMPTY; + } + + // framework/src/effects.ts + var nextId = 1; + var pending = new Map; + var queue = []; + function traceSink() { + const s = globalThis.__pocketEffectTrace; + return typeof s === "function" ? s : null; + } + function resetEffects() { + nextId = 1; + pending.clear(); + queue = []; + } + function __drainEffects() { + if (queue.length === 0) + return; + const batch = queue; + queue = []; + for (const { + id, + result + } of batch) { + const entry = pending.get(id); + if (!entry) + continue; + pending.delete(id); + traceSink()?.({ + t: "delivery", + frame: virtualFrame(), + id, + kind: entry.kind + }); + entry.onResult(result); + } + } + + // framework/src/styles.generated.ts + var STYLE_IDS = { + "flex-col items-end": 0, + "text-xs text-slate-500 tracking-wide": 1, + "w-full h-full flex-col justify-between p-5 bg-gradient-to-b from-slate-50 to-slate-100": 2, + "flex-row flex-wrap items-center justify-between": 3, + "flex-row items-center gap-3": 4, + "w-10 h-10 rounded-lg shadow": 5, + "flex-col": 6, + "text-base text-slate-950 font-bold tracking-wide": 7, + "flex-row gap-4": 8, + "text-lg text-emerald-600 font-bold": 9, + "text-lg text-blue-600 font-bold": 10, + "text-lg text-amber-600 font-bold": 11, + "flex-col gap-2": 12, + "text-xs text-blue-600 tracking-wide": 13, + "text-4xl text-slate-950 font-bold": 14, + "w-10 h-10": 15, + "h-1 w-0 rounded-full shadow bg-gradient-to-r from-blue-500 to-cyan-500": 16, + "flex-row flex-wrap gap-1": 17, + "text-sm text-slate-600": 18, + "flex-row flex-wrap items-center gap-4": 19, + "px-4 py-2 rounded-xl shadow-md bg-blue-600 border-blue-500 focus:bg-blue-500 active:bg-blue-700 transition-colors duration-150": 20, + "text-base text-white font-bold": 21, + "text-sm text-emerald-600": 22, + "relative flex-col w-full h-full bg-slate-50 overflow-hidden": 23, + "absolute inset-0 z-50 flex-col items-center justify-center": 24, + "absolute inset-0 bg-slate-950": 25, + "flex-col gap-2 w-[328] p-3 rounded-xl shadow-lg bg-white border-slate-200": 26, + "absolute left-3 right-3 bottom-3 flex-row items-center justify-between px-2 py-1 rounded-lg shadow-md bg-white border-slate-200": 27, + "flex-row flex-wrap": 28, + grow: 29 + }; + + // framework/src/index.ts + if (typeof globalThis.queueMicrotask !== "function") { + globalThis.queueMicrotask = (fn) => { + Promise.resolve().then(fn); + }; + } + var STYLES_KEY = "ui:styles"; + var FONT_PREFIX = "ui:font."; + var IMG_PREFIX = "ui:img."; + var SPRITE_PREFIX = "ui:sprite."; + function frameworkName() { + return "Solid"; + } + function globalOps() { + return globalThis.ui; + } + function uploadPakImages(ops) { + if (ops.__textures) + return; + for (const key of entries(IMG_PREFIX)) { + const blob = get(key); + let handle; + if (ops.uploadImgEntry) { + handle = ops.uploadImgEntry(blob); + } else { + const dv = new DataView(blob.buffer, blob.byteOffset, blob.byteLength); + handle = ops.uploadTexture(blob.subarray(8), dv.getUint16(0, true), dv.getUint16(2, true), blob[4]); + } + if (handle >= 0) + registerTexture(key.slice(IMG_PREFIX.length), handle); + } + } + function uploadPakSprites(ops) { + if (ops.__sprites) + return; + for (const key of entries(SPRITE_PREFIX)) { + const blob = get(key); + const dv = new DataView(blob.buffer, blob.byteOffset, blob.byteLength); + const w = dv.getUint16(0, true); + const h = dv.getUint16(2, true); + const psm = blob[4]; + const frames = dv.getUint16(6, true); + const cols = dv.getUint16(8, true); + const step = dv.getUint16(10, true); + const handle = ops.uploadTexture(blob.subarray(16), w, h, psm); + if (handle >= 0) { + registerSprite(key.slice(SPRITE_PREFIX.length), { + handle, + frames, + cols, + step + }); + } + } + } + function createLayer(style) { + const layer = createElement2("view"); + setProp(layer, "style", style, undefined); + return layer; + } + var appLayer = null; + var overlayLayer = null; + function resizeViewport(w, h) { + if (!appLayer || !overlayLayer) + return; + setProp(appLayer, "style", { + width: w, + height: h, + overflow: ENUMS.Overflow.Hidden + }, undefined); + setProp(overlayLayer, "style", { + width: w, + height: h, + posType: ENUMS.PosType.Absolute, + insetT: 0, + insetR: 0, + insetB: 0, + insetL: 0, + zIndex: 1000 + }, undefined); + const ops = getOps(); + ops.__viewport = { + w, + h + }; + } + function render2(code, opts = {}) { + const host = detectHost(opts.ops); + installHost(host); + setStyleResolver(resolveStyle); + if (opts.styles) + registerStyles(opts.styles); + const nativeTextureTable = host.kind === "native" ? host.ops.__textures : undefined; + if (host.kind === "native") { + if (nativeTextureTable) { + for (const key in nativeTextureTable) { + registerTexture(key, nativeTextureTable[key]); + } + } + const spr = host.ops.__sprites; + if (spr) { + for (const key in spr) + registerSprite(key, spr[key]); + } + } + if (host.kind === "injected" || nativeTextureTable === undefined) { + if (opts.pak) + loadPack(opts.pak); + if (hasPack()) { + for (const key of entries()) { + if (key === STYLES_KEY) { + host.ops.loadStyles?.(get(key)); + } else if (key.startsWith(FONT_PREFIX)) { + host.ops.loadFontAtlas?.(get(key)); + } + } + } + } + const viewport = hostViewport(host.ops); + const layerW = viewport?.w ?? SCREEN_W; + const layerH = viewport?.h ?? SCREEN_H; + const appRoot = createLayer({ + width: layerW, + height: layerH, + overflow: ENUMS.Overflow.Hidden + }); + const overlayRoot2 = createLayer({ + width: layerW, + height: layerH, + posType: ENUMS.PosType.Absolute, + insetT: 0, + insetR: 0, + insetB: 0, + insetL: 0, + zIndex: 1000 + }); + insertNode(rootMirror, appRoot); + insertNode(rootMirror, overlayRoot2); + setOverlayRoot(overlayRoot2); + appLayer = appRoot; + overlayLayer = overlayRoot2; + setInputRoot(appRoot); + setHitRoot(rootMirror); + resetFrameHooks(); + resetClock(); + resetEffects(); + initDevtools(host.ops); + installFrameHandler(wrapFrameHandler((buttons, analog, touches) => { + __advanceClock(); + __setAnalog(analog); + __setTouches(touches); + __drainEffects(); + runFrameHooks(buttons); + handleFrame(buttons); + runSweep(); + })); + const dispose = render(code, appRoot); + const removeResizeViewportHook = installResizeViewportHook(resizeViewport); + return () => { + removeResizeViewportHook(); + __resetTouches(); + dispose(); + setInputRoot(null); + setHitRoot(null); + setOverlayRoot(null); + appLayer = null; + overlayLayer = null; + for (const child of rootMirror.children.splice(0)) { + child.parent = null; + host.ops.destroyNode(child.id); + } + runSweep(); + }; + } + function mount(code, opts = {}) { + const ops = opts.ops ?? globalOps(); + if (!ops) { + throw new Error("PocketJS: mount() requires globalThis.ui or opts.ops"); + } + if (opts.pak) + loadPack(opts.pak); + uploadPakImages(ops); + uploadPakSprites(ops); + const dispose = render2(code, { + ops, + styles: opts.styles ?? STYLE_IDS, + pak: opts.pak + }); + return dispose; + } + + // apps/hero/app.tsx + var SPINNER_FRAME_STEP = 3; + var SPINNER_FRAMES = ["spinner-00.svg", "spinner-01.svg", "spinner-02.svg", "spinner-03.svg", "spinner-04.svg", "spinner-05.svg", "spinner-06.svg", "spinner-07.svg"]; + function Stat(props) { + return createComponent2(View, { + class: "flex-col items-end", + get children() { + return [createComponent2(Text, { + get ["class"]() { + return props.cls; + }, + get children() { + return props.value; + } + }), createComponent2(Text, { + class: "text-xs text-slate-500 tracking-wide", + get children() { + return props.label; + } + })]; + } + }); + } + function Hero() { + const [count, setCount] = createSignal(0); + const spinnerSrc = createSpriteAnimation(SPINNER_FRAMES, { + frameStep: SPINNER_FRAME_STEP + }); + let underline; + onMount(() => { + if (underline) + animate(underline, "width", 210, { + dur: 700, + easing: "out", + delay: 150 + }); + }); + return createComponent2(View, { + debugName: "HeroScreen", + class: "w-full h-full flex-col justify-between p-5 bg-gradient-to-b from-slate-50 to-slate-100", + get children() { + return [createComponent2(View, { + debugName: "Header", + class: "flex-row flex-wrap items-center justify-between", + get children() { + return [createComponent2(View, { + class: "flex-row items-center gap-3", + get children() { + return [createComponent2(Image, { + class: "w-10 h-10 rounded-lg shadow", + src: "logo.png" + }), createComponent2(View, { + class: "flex-col", + get children() { + return [createComponent2(Text, { + class: "text-base text-slate-950 font-bold tracking-wide", + children: "PocketJS" + }), createComponent2(Text, { + class: "text-xs text-slate-500 tracking-wide", + get children() { + return [memo2(() => frameworkName()), " + RUST + SCEGU"]; + } + })]; + } + })]; + } + }), createComponent2(View, { + class: "flex-row gap-4", + get children() { + return [createComponent2(Stat, { + label: "FPS", + value: "60", + cls: "text-lg text-emerald-600 font-bold" + }), createComponent2(Stat, { + label: "NODES", + value: "42", + cls: "text-lg text-blue-600 font-bold" + }), createComponent2(Stat, { + label: "DRAWS", + value: "9", + cls: "text-lg text-amber-600 font-bold" + })]; + } + })]; + } + }), createComponent2(View, { + class: "flex-col gap-2", + get children() { + return [createComponent2(Text, { + class: "text-xs text-blue-600 tracking-wide", + children: "ONE RUST CORE · ONE JSX APP" + }), createComponent2(View, { + class: "flex-row flex-wrap items-center justify-between", + get children() { + return [createComponent2(Text, { + class: "text-4xl text-slate-950 font-bold", + children: "JSX at 60 FPS." + }), createComponent2(Image, { + class: "w-10 h-10", + get src() { + return spinnerSrc(); + } + })]; + } + }), createComponent2(View, { + ref(r$) { + var _ref$ = underline; + typeof _ref$ === "function" ? _ref$(r$) : underline = r$; + }, + class: "h-1 w-0 rounded-full shadow bg-gradient-to-r from-blue-500 to-cyan-500", + get style() { + return { + translateX: count() * 2 + }; + } + }), createComponent2(View, { + debugName: "Description", + class: "flex-row flex-wrap gap-1", + get children() { + return [createComponent2(Text, { + class: "text-sm text-slate-600", + children: "Flexbox, springs and baked type —" + }), createComponent2(Text, { + class: "text-sm text-slate-600", + children: "running on a 2005 handheld." + })]; + } + })]; + } + }), createComponent2(View, { + class: "flex-row flex-wrap items-center gap-4", + get children() { + return [createComponent2(View, { + class: "px-4 py-2 rounded-xl shadow-md bg-blue-600 border-blue-500 focus:bg-blue-500 active:bg-blue-700 transition-colors duration-150", + focusable: true, + onPress: () => setCount(count() + 1), + get children() { + return createComponent2(Text, { + class: "text-base text-white font-bold", + children: "Press Circle" + }); + } + }), createComponent2(Text, { + class: "text-sm text-slate-600", + get children() { + return ["Count: ", memo2(() => count())]; + } + }), createComponent2(Show, { + get when() { + return count() > 3; + }, + get children() { + return createComponent2(Text, { + class: "text-sm text-emerald-600", + children: "Reactive on real hardware." + }); + } + })]; + } + })]; + } + }); + } + + // apps/hero/main.tsx + mount(() => createComponent2(Hero, {})); +})(); diff --git a/hosts/wm6/vs2005/prebuilt/PocketJS.WM6.Demo.pak b/hosts/wm6/vs2005/prebuilt/PocketJS.WM6.Demo.pak new file mode 100644 index 000000000..e2a8503e2 Binary files /dev/null and b/hosts/wm6/vs2005/prebuilt/PocketJS.WM6.Demo.pak differ diff --git a/hosts/wm6/vs2005/prebuilt/PocketJS.WM6.QuickJS.Probe.exe b/hosts/wm6/vs2005/prebuilt/PocketJS.WM6.QuickJS.Probe.exe new file mode 100644 index 000000000..2ac2e860b Binary files /dev/null and b/hosts/wm6/vs2005/prebuilt/PocketJS.WM6.QuickJS.Probe.exe differ diff --git a/hosts/wm6/vs2005/prebuilt/PocketJS.WM6.QuickJS.v3.dll b/hosts/wm6/vs2005/prebuilt/PocketJS.WM6.QuickJS.v3.dll new file mode 100644 index 000000000..0f11a597f Binary files /dev/null and b/hosts/wm6/vs2005/prebuilt/PocketJS.WM6.QuickJS.v3.dll differ diff --git a/hosts/wm6/vs2005/rebuild-release.cmd b/hosts/wm6/vs2005/rebuild-release.cmd new file mode 100644 index 000000000..cb9068968 --- /dev/null +++ b/hosts/wm6/vs2005/rebuild-release.cmd @@ -0,0 +1,11 @@ +@echo off +setlocal +if not defined VS80COMNTOOLS ( + echo Run this script from a Visual Studio 2005 Command Prompt. + exit /b 2 +) +pushd "%~dp0" +"%VS80COMNTOOLS%..\IDE\devenv.com" "PocketJS.WM6.sln" /Rebuild "Release|Windows Mobile 6 Professional SDK (ARMV4I)" /Out "rebuild-release.log" +set "WM6_BUILD_RESULT=%ERRORLEVEL%" +popd +exit /b %WM6_BUILD_RESULT% diff --git a/hosts/wm6/vs2005/resources/probe.rc b/hosts/wm6/vs2005/resources/probe.rc new file mode 100644 index 000000000..f55ecb0be --- /dev/null +++ b/hosts/wm6/vs2005/resources/probe.rc @@ -0,0 +1,4 @@ +// Tell Windows Mobile that the application handles the device's native DPI. +// Without this resource, VGA Pocket PCs expose a virtual 240x320 QVGA screen +// and scale the application's output for backward compatibility. +HI_RES_AWARE CEUX { 1 } diff --git a/hosts/wm6/vs2005/runtime/vapor.h b/hosts/wm6/vs2005/runtime/vapor.h new file mode 100644 index 000000000..bed5ccd06 --- /dev/null +++ b/hosts/wm6/vs2005/runtime/vapor.h @@ -0,0 +1,116 @@ +/* vapor/runtime/vapor.h — the Pocket Vapor runtime contract, all targets. + * + * Two parties compile against this header: the fixed per-console runtime + * (gba/vapor_gba.c, gb/vapor_gb.c, nes/vapor_nes.c, + * esp32/vapor_esp32.c) and the compiler-generated application (gen_app.c). + * The runtime owns the cell grid, video commit, input edges, the frame loop + * and the debug block; the generated + * app owns all reactive state, computeds, paint effects and button + * handlers. No allocator exists anywhere — every byte is planned at + * compile time. + * + * Portability: this compiles under arm-none-eabi-gcc (ARM7TDMI), sdcc + * (SM83), cc65 (6502), and xtensa-esp-elf-gcc. `int` is 16-bit on the + * 8-bit consoles, so the 32-bit types are `long`; cc65 is C89, so + * `inline` vanishes there. + */ +#ifndef POCKET_VAPOR_H +#define POCKET_VAPOR_H + +typedef unsigned char u8; +typedef unsigned short u16; +typedef unsigned long u32; +typedef signed char s8; +typedef signed short s16; +typedef signed long s32; + +#if defined(__CC65__) +#define inline +#endif + +/* Geometry + budgets: per-target, injected by the compiler as #defines in + * gen_app.c before this header; the defaults are the GBA profile. */ +#ifndef VP_GRID_W +#define VP_GRID_W 30 +#endif +#ifndef VP_GRID_H +#define VP_GRID_H 20 +#endif +#ifndef VP_STR_CAP +#define VP_STR_CAP 24 /* max chars in a pooled/scratch string */ +#endif +#ifndef VP_VIEW_CAP +#define VP_VIEW_CAP 32 /* max elements a list view can hold */ +#endif + +/* A bounded string: len + bytes, no heap, no NUL required. */ +typedef struct { + u8 len; + char b[VP_STR_CAP]; +} vp_sb; + +/* A list view: indices into an app pool, produced by filter/slice chains. */ +typedef struct { + u8 len; + u8 idx[VP_VIEW_CAP]; +} vp_view; + +/* ---- grid (runtime-owned) -------------------------------------------------- */ +void vp_row_clear(u8 y0, u8 y1); /* rows [y0, y1): space, pair 0 */ + +/* Row painting is compose-then-commit: parts append into a line scratch, + * commit places the line (left at x / centered / right), painting the FULL + * row in the row's style pair — a row owns its whole line. */ +#define VP_ALIGN_LEFT 0 +#define VP_ALIGN_CENTER 1 +#define VP_ALIGN_RIGHT 2 +void vp_ln_reset(void); +void vp_ln_str(const char *s); +void vp_ln_sb(const vp_sb *s); +void vp_ln_ch(char c); +void vp_ln_int(s32 v); +void vp_ln_commit(u8 y, u8 x, u8 pal, u8 align); + +/* ---- strings ---------------------------------------------------------------- */ +void vp_sb_reset(vp_sb *s); +void vp_sb_str(vp_sb *s, const char *lit); +void vp_sb_sb(vp_sb *s, const vp_sb *src); +void vp_sb_ch(vp_sb *s, char c); +/* JS String.prototype.slice(start, end) with clamping, ASCII only. */ +void vp_sb_slice(vp_sb *dst, const vp_sb *src, s32 start, s32 end); +/* Assign tmp into dst; returns 1 if the value changed (Vue set semantics). */ +u8 vp_sb_assign(vp_sb *dst, const vp_sb *tmp); +u8 vp_sb_eq(const vp_sb *a, const vp_sb *b); + +/* ---- tripwires -------------------------------------------------------------- */ +#define VP_TRIP_POOL_FULL 1 +#define VP_TRIP_STR_TRUNC 2 +#define VP_TRIP_VIEW_FULL 4 +extern u8 vp_tripwires; + +/* core state shared with the per-target runtime */ +extern u32 vp_rows_dirty; +extern const u32 vp_bit32[32]; /* vp_bit32[n] == 1UL << n */ + +/* ---- generated app hooks ----------------------------------------------------- */ +void app_init(void); /* seed state + first paint (all effects) */ +void app_on_button(u8 b); /* one press edge, GBA key bit index */ +u8 app_flush(void); /* computeds + dirty effects; 1 if painted */ +u16 app_debug_state(volatile u8 *out); /* mirror reactive state; returns bytes */ + +/* generated data the runtime uploads at boot (per-target encodings): + * GBA: vp_font_tiles 95x32B 4bpp, vp_palettes/vp_palette_count/vp_backdrop + * GB: vp_font_tiles (2 styles x 95) x 16B 2bpp interleaved + * NES: vp_font_tiles (2 styles x 95) x 16B 2bpp planar + * ESP32: vp_font_tiles 95x8B 1bpp, direct RGB565 ink/paper tables + * GB/NES: vp_pal_style[8] maps logical palette -> glyph style (0/1) */ +extern const u8 vp_font_tiles[]; +extern const u16 vp_palettes[]; +extern const u8 vp_palette_count; +extern const u16 vp_backdrop; +extern const u16 vp_ink565[]; +extern const u16 vp_paper565[]; +extern const u8 vp_pal_style[]; +extern const char vp_app_title[]; /* cartridge title, <= 12 chars */ + +#endif diff --git a/hosts/wm6/vs2005/runtime/vapor_core.c b/hosts/wm6/vs2005/runtime/vapor_core.c new file mode 100644 index 000000000..ec91267b1 --- /dev/null +++ b/hosts/wm6/vs2005/runtime/vapor_core.c @@ -0,0 +1,143 @@ +/* vapor/runtime/vapor_core.c — target-independent runtime half. + * + * The cell grid IS the debug block: each target defines vp_grid_ch / + * vp_grid_pal at its console's fixed debug address (GBA copies to EWRAM + * instead — it can afford to), so the harness reads the same logical screen + * the paint effects wrote, regardless of how far the console's vblank + * budget has gotten with the physical VRAM commit. Compiled per target + * with the same VP_* defines as gen_app.c. + */ +#include "vapor.h" + +u8 vp_tripwires; +u32 vp_rows_dirty; + +/* Bit table instead of `(u32)1 << n`: sdcc 4.6 (SM83) miscompiles some + * u8-operand shifts/multiplies, and 6502 variable long shifts are slow. */ +const u32 vp_bit32[32] = { + 0x1UL, 0x2UL, 0x4UL, 0x8UL, 0x10UL, 0x20UL, 0x40UL, 0x80UL, + 0x100UL, 0x200UL, 0x400UL, 0x800UL, 0x1000UL, 0x2000UL, 0x4000UL, 0x8000UL, + 0x10000UL, 0x20000UL, 0x40000UL, 0x80000UL, 0x100000UL, 0x200000UL, 0x400000UL, 0x800000UL, + 0x1000000UL, 0x2000000UL, 0x4000000UL, 0x8000000UL, 0x10000000UL, 0x20000000UL, 0x40000000UL, 0x80000000UL, +}; + +/* u16 row math: u8*u8 would lower to sdcc's buggy __muluchar */ +#define ROW_CH(y) ((u8 *)vp_grid_ch + (u16)(y) * VP_GRID_W) +#define ROW_PAL(y) ((u8 *)vp_grid_pal + (u16)(y) * VP_GRID_W) + +/* defined by the per-target runtime, at that console's debug address */ +extern u8 vp_grid_ch[VP_GRID_H][VP_GRID_W]; +extern u8 vp_grid_pal[VP_GRID_H][VP_GRID_W]; + +static void cell(u8 y, u8 x, u8 ch, u8 pal) { + u8 *pc = ROW_CH(y) + x; + u8 *pp = ROW_PAL(y) + x; + if (*pc == ch && *pp == pal) return; + *pc = ch; + *pp = pal; + vp_rows_dirty |= vp_bit32[y]; +} + +void vp_row_clear(u8 y0, u8 y1) { + u8 y, x; + for (y = y0; y < y1 && y < VP_GRID_H; y++) + for (x = 0; x < VP_GRID_W; x++) cell(y, x, ' ', 0); +} + +/* ---- line compose ------------------------------------------------------------ */ +static u8 vp_ln[VP_GRID_W]; +static u8 vp_ln_len; + +void vp_ln_reset(void) { vp_ln_len = 0; } + +void vp_ln_ch(char c) { + u8 ch = (u8)c; + if (vp_ln_len >= VP_GRID_W) return; /* clip at the row edge */ + if (ch < 0x20 || ch > 0x7e) ch = '?'; + vp_ln[vp_ln_len++] = ch; +} + +void vp_ln_str(const char *s) { + while (*s) vp_ln_ch(*s++); +} + +void vp_ln_sb(const vp_sb *s) { + u8 i; + for (i = 0; i < s->len; i++) vp_ln_ch(s->b[i]); +} + +void vp_ln_int(s32 v) { + char buf[12]; + u8 n = 0; + u32 mag; + if (v < 0) { + vp_ln_ch('-'); + mag = (u32)(-v); + } else { + mag = (u32)v; + } + do { + buf[n++] = (char)('0' + (u8)(mag % 10)); + mag /= 10; + } while (mag && n < 11); + while (n) vp_ln_ch(buf[--n]); +} + +void vp_ln_commit(u8 y, u8 x, u8 pal, u8 align) { + u8 start, col; + if (align == VP_ALIGN_CENTER) start = (u8)((VP_GRID_W - vp_ln_len) >> 1); + else if (align == VP_ALIGN_RIGHT) start = (u8)(VP_GRID_W - vp_ln_len); + else start = x; + if (start >= VP_GRID_W) start = 0; + for (col = 0; col < VP_GRID_W; col++) { + u8 ch = ' '; + if (col >= start && (u8)(col - start) < vp_ln_len) ch = vp_ln[col - start]; + cell(y, col, ch, pal); + } +} + +/* ---- strings ---------------------------------------------------------------- */ +void vp_sb_reset(vp_sb *s) { s->len = 0; } + +void vp_sb_ch(vp_sb *s, char c) { + if (s->len >= VP_STR_CAP) { + vp_tripwires |= VP_TRIP_STR_TRUNC; + return; + } + s->b[s->len++] = c; +} + +void vp_sb_str(vp_sb *s, const char *lit) { + while (*lit) vp_sb_ch(s, *lit++); +} + +void vp_sb_sb(vp_sb *s, const vp_sb *src) { + u8 i; + for (i = 0; i < src->len; i++) vp_sb_ch(s, src->b[i]); +} + +void vp_sb_slice(vp_sb *dst, const vp_sb *src, s32 start, s32 end) { + s32 len = src->len, i; + if (start < 0) start += len; + if (end < 0) end += len; + if (start < 0) start = 0; + if (end > len) end = len; + dst->len = 0; + for (i = start; i < end; i++) vp_sb_ch(dst, src->b[i]); +} + +u8 vp_sb_eq(const vp_sb *a, const vp_sb *b) { + u8 i; + if (a->len != b->len) return 0; + for (i = 0; i < a->len; i++) + if (a->b[i] != b->b[i]) return 0; + return 1; +} + +u8 vp_sb_assign(vp_sb *dst, const vp_sb *tmp) { + u8 i; + if (vp_sb_eq(dst, tmp)) return 0; + dst->len = tmp->len; + for (i = 0; i < tmp->len; i++) dst->b[i] = tmp->b[i]; + return 1; +} diff --git a/hosts/wm6/vs2005/runtime/wm6_quickjs_abi.h b/hosts/wm6/vs2005/runtime/wm6_quickjs_abi.h new file mode 100644 index 000000000..fa1289ef4 --- /dev/null +++ b/hosts/wm6/vs2005/runtime/wm6_quickjs_abi.h @@ -0,0 +1,53 @@ +#ifndef POCKETJS_WM6_QUICKJS_ABI_H +#define POCKETJS_WM6_QUICKJS_ABI_H + +#define WM6_QJS_ABI_VERSION 3u + +#if defined(__cplusplus) +extern "C" { +#endif + +typedef void *wm6_qjs_handle; + +typedef unsigned int (__cdecl *wm6_qjs_abi_version_fn)(void); +typedef wm6_qjs_handle (__cdecl *wm6_qjs_create_fn)( + unsigned int memory_limit, + unsigned int stack_limit, + unsigned int viewport_width, + unsigned int viewport_height, + char *error, + unsigned int error_capacity); +typedef int (__cdecl *wm6_qjs_set_pak_fn)( + wm6_qjs_handle handle, + const unsigned char *data, + unsigned int data_length, + char *error, + unsigned int error_capacity); +typedef int (__cdecl *wm6_qjs_eval_fn)( + wm6_qjs_handle handle, + const char *source, + unsigned int source_length, + char *output, + unsigned int output_capacity); +typedef int (__cdecl *wm6_qjs_drain_jobs_fn)( + wm6_qjs_handle handle, + char *output, + unsigned int output_capacity); +typedef const unsigned char *(__cdecl *wm6_qjs_frame_fn)( + wm6_qjs_handle handle, + unsigned int buttons, + const unsigned int *touches, + unsigned int touch_count, + unsigned int *width, + unsigned int *height, + unsigned int *stride, + unsigned int *byte_length, + char *error, + unsigned int error_capacity); +typedef void (__cdecl *wm6_qjs_destroy_fn)(wm6_qjs_handle handle); + +#if defined(__cplusplus) +} +#endif + +#endif diff --git a/hosts/wm6/vs2005/src/main.cpp b/hosts/wm6/vs2005/src/main.cpp new file mode 100644 index 000000000..cf73ff90e --- /dev/null +++ b/hosts/wm6/vs2005/src/main.cpp @@ -0,0 +1,372 @@ +// PocketJS Windows Mobile 6 hardware probe. +// +// Keep this translation unit compatible with the Visual C++ 2005 compiler: +// no C++11, no desktop-only Win32 calls, and no dependency on MFC or ATL. + +#include +#include +#include + +namespace { + +const TCHAR kWindowClass[] = _T("PocketJS.WM6.Probe"); +const UINT_PTR kFrameTimer = 1; +const UINT kFramePeriodMs = 33; + +struct ProbeState { + DWORD startedAt; + DWORD frames; + DWORD lastFrameAt; + DWORD framePeriod; + DWORD lastKey; + POINT pointer; + BOOL pointerDown; + int width; + int height; + HDC backBuffer; + HBITMAP backBitmap; + HBITMAP backPreviousBitmap; + int backWidth; + int backHeight; +}; + +ProbeState g_state; + +int Clamp(int value, int minimum, int maximum) +{ + if (value < minimum) return minimum; + if (value > maximum) return maximum; + return value; +} + +void FillRectColor(HDC dc, int left, int top, int right, int bottom, COLORREF color) +{ + RECT rect; + HBRUSH brush; + rect.left = left; + rect.top = top; + rect.right = right; + rect.bottom = bottom; + brush = CreateSolidBrush(color); + if (brush != NULL) { + FillRect(dc, &rect, brush); + DeleteObject(brush); + } +} + +void ReleaseBackBuffer() +{ + if (g_state.backBuffer != NULL && g_state.backPreviousBitmap != NULL) { + SelectObject(g_state.backBuffer, g_state.backPreviousBitmap); + } + if (g_state.backBitmap != NULL) DeleteObject(g_state.backBitmap); + if (g_state.backBuffer != NULL) DeleteDC(g_state.backBuffer); + g_state.backBuffer = NULL; + g_state.backBitmap = NULL; + g_state.backPreviousBitmap = NULL; + g_state.backWidth = 0; + g_state.backHeight = 0; +} + +BOOL EnsureBackBuffer(HDC target, int width, int height) +{ + HDC nextBuffer; + HBITMAP nextBitmap; + HBITMAP nextPreviousBitmap; + + if ( + g_state.backBuffer != NULL && + g_state.backWidth == width && + g_state.backHeight == height + ) { + return TRUE; + } + + ReleaseBackBuffer(); + nextBuffer = CreateCompatibleDC(target); + nextBitmap = CreateCompatibleBitmap(target, width, height); + if (nextBuffer == NULL || nextBitmap == NULL) { + if (nextBitmap != NULL) DeleteObject(nextBitmap); + if (nextBuffer != NULL) DeleteDC(nextBuffer); + return FALSE; + } + nextPreviousBitmap = static_cast(SelectObject(nextBuffer, nextBitmap)); + if (nextPreviousBitmap == NULL) { + DeleteObject(nextBitmap); + DeleteDC(nextBuffer); + return FALSE; + } + + g_state.backBuffer = nextBuffer; + g_state.backBitmap = nextBitmap; + g_state.backPreviousBitmap = nextPreviousBitmap; + g_state.backWidth = width; + g_state.backHeight = height; + return TRUE; +} + +HFONT CreateProbeFont() +{ + const TCHAR faceName[] = _T("Tahoma"); + LOGFONT description; + int index; + ZeroMemory(&description, sizeof(description)); + description.lfHeight = -16; + description.lfWeight = FW_NORMAL; + description.lfCharSet = DEFAULT_CHARSET; + description.lfOutPrecision = OUT_DEFAULT_PRECIS; + description.lfClipPrecision = CLIP_DEFAULT_PRECIS; + description.lfQuality = DEFAULT_QUALITY; + description.lfPitchAndFamily = DEFAULT_PITCH | FF_DONTCARE; + for ( + index = 0; + index < LF_FACESIZE - 1 && faceName[index] != _T('\0'); + ++index + ) { + description.lfFaceName[index] = faceName[index]; + } + description.lfFaceName[index] = _T('\0'); + return CreateFontIndirect(&description); +} + +void DrawProbe(HWND window, HDC target) +{ + RECT client; + HDC back; + HFONT font; + HFONT previousFont; + MEMORYSTATUS memory; + TCHAR line[256]; + DWORD elapsed; + int width; + int height; + int stripe; + + GetClientRect(window, &client); + width = client.right - client.left; + height = client.bottom - client.top; + if (width <= 0 || height <= 0) return; + + if (!EnsureBackBuffer(target, width, height)) return; + back = g_state.backBuffer; + + FillRectColor(back, 0, 0, width, height, RGB(12, 16, 24)); + for (stripe = 0; stripe < 6; ++stripe) { + const int left = (width * stripe) / 6; + const int right = (width * (stripe + 1)) / 6; + FillRectColor( + back, + left, + 0, + right, + 12, + RGB(30 + stripe * 24, 170 - stripe * 12, 210 - stripe * 18) + ); + } + + font = CreateProbeFont(); + previousFont = NULL; + if (font != NULL) { + previousFont = static_cast(SelectObject(back, font)); + } + SetBkMode(back, TRANSPARENT); + SetTextColor(back, RGB(238, 243, 250)); + + RECT textRect; + textRect.left = 18; + textRect.top = 28; + textRect.right = width - 18; + textRect.bottom = height - 18; + DrawText( + back, + _T("PocketJS / WM6 hardware probe\r\n") + _T("VS2005 + ARMV4I + native Win32"), + -1, + &textRect, + DT_LEFT | DT_TOP | DT_NOPREFIX + ); + + ZeroMemory(&memory, sizeof(memory)); + memory.dwLength = sizeof(memory); + GlobalMemoryStatus(&memory); + elapsed = GetTickCount() - g_state.startedAt; + + wsprintf( + line, + _T("screen: %d x %d\r\n") + _T("client: %d x %d\r\n") + _T("RAM free: %lu / %lu KB\r\n") + _T("frames: %lu last: %lu ms\r\n") + _T("key: 0x%02lX\r\n") + _T("touch: %ld, %ld %s\r\n") + _T("uptime: %lu ms"), + GetSystemMetrics(SM_CXSCREEN), + GetSystemMetrics(SM_CYSCREEN), + width, + height, + memory.dwAvailPhys / 1024, + memory.dwTotalPhys / 1024, + g_state.frames, + g_state.framePeriod, + g_state.lastKey, + g_state.pointer.x, + g_state.pointer.y, + g_state.pointerDown ? _T("DOWN") : _T("up"), + elapsed + ); + textRect.top = 92; + DrawText(back, line, -1, &textRect, DT_LEFT | DT_TOP | DT_NOPREFIX); + + const int markerX = Clamp(g_state.pointer.x, 0, width - 1); + const int markerY = Clamp(g_state.pointer.y, 0, height - 1); + const COLORREF markerColor = + g_state.pointerDown ? RGB(255, 194, 74) : RGB(90, 210, 255); + HPEN pen = CreatePen(PS_SOLID, 3, markerColor); + if (pen != NULL) { + HPEN previousPen = static_cast(SelectObject(back, pen)); + MoveToEx(back, markerX - 12, markerY, NULL); + LineTo(back, markerX + 13, markerY); + MoveToEx(back, markerX, markerY - 12, NULL); + LineTo(back, markerX, markerY + 13); + SelectObject(back, previousPen); + DeleteObject(pen); + } + + BitBlt(target, 0, 0, width, height, back, 0, 0, SRCCOPY); + + if (previousFont != NULL) SelectObject(back, previousFont); + if (font != NULL) DeleteObject(font); +} + +void UpdatePointer(LPARAM parameter, BOOL down) +{ + g_state.pointer.x = static_cast(LOWORD(parameter)); + g_state.pointer.y = static_cast(HIWORD(parameter)); + g_state.pointerDown = down; +} + +LRESULT CALLBACK WindowProcedure(HWND window, UINT message, WPARAM wParam, LPARAM lParam) +{ + switch (message) { + case WM_CREATE: + g_state.startedAt = GetTickCount(); + g_state.lastFrameAt = g_state.startedAt; + g_state.pointer.x = GetSystemMetrics(SM_CXSCREEN) / 2; + g_state.pointer.y = GetSystemMetrics(SM_CYSCREEN) / 2; + SetTimer(window, kFrameTimer, kFramePeriodMs, NULL); + return 0; + + case WM_SIZE: + g_state.width = LOWORD(lParam); + g_state.height = HIWORD(lParam); + InvalidateRect(window, NULL, FALSE); + return 0; + + case WM_TIMER: + if (wParam == kFrameTimer) { + const DWORD now = GetTickCount(); + g_state.framePeriod = now - g_state.lastFrameAt; + g_state.lastFrameAt = now; + ++g_state.frames; + InvalidateRect(window, NULL, FALSE); + } + return 0; + + case WM_KEYDOWN: + g_state.lastKey = static_cast(wParam); + if (wParam == VK_ESCAPE) { + DestroyWindow(window); + } else { + InvalidateRect(window, NULL, FALSE); + } + return 0; + + case WM_LBUTTONDOWN: + SetCapture(window); + UpdatePointer(lParam, TRUE); + InvalidateRect(window, NULL, FALSE); + return 0; + + case WM_MOUSEMOVE: + if ((wParam & MK_LBUTTON) != 0) { + UpdatePointer(lParam, TRUE); + InvalidateRect(window, NULL, FALSE); + } + return 0; + + case WM_LBUTTONUP: + ReleaseCapture(); + UpdatePointer(lParam, FALSE); + InvalidateRect(window, NULL, FALSE); + return 0; + + case WM_ERASEBKGND: + return 1; + + case WM_PAINT: + { + PAINTSTRUCT paint; + HDC dc = BeginPaint(window, &paint); + DrawProbe(window, dc); + EndPaint(window, &paint); + } + return 0; + + case WM_DESTROY: + KillTimer(window, kFrameTimer); + ReleaseBackBuffer(); + PostQuitMessage(0); + return 0; + } + return DefWindowProc(window, message, wParam, lParam); +} + +} // namespace + +int WINAPI WinMain(HINSTANCE instance, HINSTANCE, LPWSTR, int showCommand) +{ + WNDCLASS windowClass; + HWND window; + MSG message; + BOOL messageResult; + + ZeroMemory(&g_state, sizeof(g_state)); + ZeroMemory(&windowClass, sizeof(windowClass)); + windowClass.style = CS_HREDRAW | CS_VREDRAW; + windowClass.lpfnWndProc = WindowProcedure; + windowClass.hInstance = instance; + windowClass.hCursor = LoadCursor(NULL, IDC_ARROW); + windowClass.hbrBackground = static_cast(GetStockObject(BLACK_BRUSH)); + windowClass.lpszClassName = kWindowClass; + + if (!RegisterClass(&windowClass) && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) { + return 1; + } + + SHInitExtraControls(); + window = CreateWindow( + kWindowClass, + _T("PocketJS WM6 Probe"), + WS_VISIBLE, + 0, + 0, + GetSystemMetrics(SM_CXSCREEN), + GetSystemMetrics(SM_CYSCREEN), + NULL, + NULL, + instance, + NULL + ); + if (window == NULL) return 2; + + SHFullScreen(window, SHFS_HIDETASKBAR | SHFS_HIDESIPBUTTON | SHFS_HIDESTARTICON); + ShowWindow(window, showCommand); + UpdateWindow(window); + + while ((messageResult = GetMessage(&message, NULL, 0, 0)) > 0) { + TranslateMessage(&message); + DispatchMessage(&message); + } + if (messageResult < 0) return 3; + return static_cast(message.wParam); +} diff --git a/hosts/wm6/vs2005/src/quickjs_deploy.c b/hosts/wm6/vs2005/src/quickjs_deploy.c new file mode 100644 index 000000000..c6c9a110b --- /dev/null +++ b/hosts/wm6/vs2005/src/quickjs_deploy.c @@ -0,0 +1,828 @@ +#include +#include + +#include "wm6_quickjs_abi.h" +#include "wm6_framebuffer.h" + +static int append_file_name(WCHAR *path, unsigned int capacity, + const WCHAR *name) +{ + unsigned int length; + unsigned int index; + + length = 0; + while (path[length] != L'\0') + length++; + while (length > 0 && path[length - 1] != L'\\') + length--; + index = 0; + while (name[index] != L'\0' && length + index + 1 < capacity) { + path[length + index] = name[index]; + index++; + } + path[length + index] = L'\0'; + return name[index] == L'\0'; +} + +static unsigned char *read_neighbor_file( + const WCHAR *name, + unsigned int *length) +{ + WCHAR path[MAX_PATH]; + HANDLE file; + DWORD size; + DWORD read; + unsigned char *bytes; + + *length = 0; + if (!GetModuleFileName(NULL, path, MAX_PATH)) + return NULL; + if (!append_file_name(path, MAX_PATH, name)) + return NULL; + file = CreateFile(path, GENERIC_READ, FILE_SHARE_READ, NULL, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (file == INVALID_HANDLE_VALUE) + return NULL; + size = GetFileSize(file, NULL); + if (size == INVALID_FILE_SIZE || size == 0) { + CloseHandle(file); + return NULL; + } + bytes = (unsigned char *)LocalAlloc(LMEM_FIXED, size + 1); + if (!bytes) { + CloseHandle(file); + return NULL; + } + if (!ReadFile(file, bytes, size, &read, NULL) || read != size) { + LocalFree(bytes); + CloseHandle(file); + return NULL; + } + CloseHandle(file); + bytes[size] = '\0'; + *length = size; + return bytes; +} + +static void ascii_to_wide(WCHAR *output, unsigned int capacity, + const char *text) +{ + unsigned int index; + + if (capacity == 0) + return; + index = 0; + while (text && text[index] != '\0' && index + 1 < capacity) { + unsigned char ch = (unsigned char)text[index]; + output[index] = ch < 128 ? (WCHAR)ch : L'?'; + index++; + } + output[index] = L'\0'; +} + +static int g_framebuffer_ready; +static int g_frame_available; +static HMODULE g_quickjs_module; +static wm6_qjs_handle g_quickjs_runtime; +static wm6_qjs_frame_fn g_quickjs_frame; +static wm6_qjs_destroy_fn g_quickjs_destroy; +static unsigned int g_buttons; +static unsigned int g_pressed_buttons; +static int g_viewport_width; +static int g_viewport_height; +static int g_display_width; +static int g_display_height; +static int g_touch_active; +static int g_touch_x; +static int g_touch_y; +static int g_frame_error_shown; +static int g_first_frame_reported; +static DWORD g_frame_window_started; +static unsigned int g_frame_window_count; +static DWORD g_profile_core_ms; +static DWORD g_profile_copy_ms; +static DWORD g_profile_present_ms; +static DEVMODE g_original_display_mode; +static int g_display_rotated; +static int g_shell_hidden; +static int g_taskbar_hidden; +static HMODULE g_aygshell_module; +typedef BOOL (WINAPI *wm6_sh_fullscreen_fn)(HWND, DWORD); +static wm6_sh_fullscreen_fn g_sh_fullscreen; + +static void restore_display_orientation(void); + +static void enter_fullscreen(HWND window) +{ + DWORD state; + HWND taskbar; + + MoveWindow( + window, + 0, + 0, + GetSystemMetrics(SM_CXSCREEN), + GetSystemMetrics(SM_CYSCREEN), + TRUE); + SetForegroundWindow(window); + state = SHFS_HIDETASKBAR | + SHFS_HIDESTARTICON | + SHFS_HIDESIPBUTTON; + g_aygshell_module = LoadLibrary(L"aygshell.dll"); + if (g_aygshell_module) { + g_sh_fullscreen = (wm6_sh_fullscreen_fn)GetProcAddress( + g_aygshell_module, + L"SHFullScreen"); + } + if (g_sh_fullscreen && g_sh_fullscreen(window, state)) { + g_shell_hidden = 1; + OutputDebugString( + L"PocketJS WM6: shell chrome hidden\r\n"); + return; + } + + taskbar = FindWindow(L"HHTaskBar", NULL); + if (taskbar) { + ShowWindow(taskbar, SW_HIDE); + g_taskbar_hidden = 1; + g_shell_hidden = 1; + OutputDebugString( + L"PocketJS WM6: shell taskbar hidden by fallback\r\n"); + } else { + OutputDebugString( + L"PocketJS WM6: shell chrome could not be hidden\r\n"); + } +} + +static void leave_fullscreen(HWND window) +{ + DWORD state; + HWND taskbar; + + if (g_shell_hidden && g_sh_fullscreen) { + state = SHFS_SHOWTASKBAR | + SHFS_SHOWSTARTICON | + SHFS_SHOWSIPBUTTON; + g_sh_fullscreen(window, state); + } + if (g_taskbar_hidden) { + taskbar = FindWindow(L"HHTaskBar", NULL); + if (taskbar) + ShowWindow(taskbar, SW_SHOW); + } + if (g_aygshell_module) + FreeLibrary(g_aygshell_module); + g_shell_hidden = 0; + g_taskbar_hidden = 0; + g_sh_fullscreen = NULL; + g_aygshell_module = NULL; +} + +static int rotate_display_90(void) +{ + DEVMODE current; + DEVMODE requested; + LONG status; + + memset(¤t, 0, sizeof(current)); + current.dmSize = sizeof(current); + if (!EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, ¤t)) + return 0; + if (current.dmDisplayOrientation == DMDO_90 && + GetSystemMetrics(SM_CXSCREEN) > GetSystemMetrics(SM_CYSCREEN)) + return 1; + g_original_display_mode = current; + requested = current; + requested.dmFields = DM_DISPLAYORIENTATION; + requested.dmDisplayOrientation = DMDO_90; + status = ChangeDisplaySettingsEx( + NULL, &requested, NULL, CDS_TEST, NULL); + if (status != DISP_CHANGE_SUCCESSFUL) + return 0; + /* + * Windows CE also uses CDS_TEST with DM_DISPLAYORIENTATION to return the + * current orientation in this field, so restore the requested value. + */ + requested.dmDisplayOrientation = DMDO_90; + status = ChangeDisplaySettingsEx( + NULL, &requested, NULL, CDS_RESET, NULL); + if (status != DISP_CHANGE_SUCCESSFUL) + return 0; + g_display_rotated = 1; + if (GetSystemMetrics(SM_CXSCREEN) <= GetSystemMetrics(SM_CYSCREEN)) { + restore_display_orientation(); + return 0; + } + return 1; +} + +static void restore_display_orientation(void) +{ + DEVMODE requested; + + if (!g_display_rotated) + return; + requested = g_original_display_mode; + requested.dmFields = DM_DISPLAYORIENTATION; + ChangeDisplaySettingsEx(NULL, &requested, NULL, CDS_RESET, NULL); + g_display_rotated = 0; +} + +static unsigned int button_for_key(WPARAM key) +{ + switch (key) { + case VK_UP: + return 0x0010u; + case VK_RIGHT: + return 0x0020u; + case VK_DOWN: + return 0x0040u; + case VK_LEFT: + return 0x0080u; + case VK_RETURN: + case VK_SPACE: + return 0x2000u; + } + return 0; +} + +static void update_touch_position(LPARAM position) +{ + int x; + int y; + + x = (short)LOWORD(position); + y = (short)HIWORD(position); + if (x < 0) + x = 0; + else if (x >= g_display_width) + x = g_display_width - 1; + if (y < 0) + y = 0; + else if (y >= g_display_height) + y = g_display_height - 1; + if (g_display_width > 0) + x = x * g_viewport_width / g_display_width; + if (g_display_height > 0) + y = y * g_viewport_height / g_display_height; + g_touch_x = x; + g_touch_y = y; +} + +static int stop_frame_rendering(const WCHAR *message) +{ + if (!g_frame_error_shown) { + g_frame_error_shown = 1; + OutputDebugString(L"PocketJS WM6 frame failure: "); + OutputDebugString(message); + OutputDebugString(L"\r\n"); + MessageBox(NULL, message, L"PocketJS frame failed", MB_OK); + } + g_framebuffer_ready = 0; + g_frame_available = 0; + return 0; +} + +static void report_first_frame_pixels( + const unsigned char *pixels, + unsigned int width, + unsigned int height, + unsigned int stride, + unsigned int byte_length) +{ + WCHAR receipt[256]; + DWORD alpha_pixels; + DWORD colored_pixels; + unsigned int row; + + if (g_first_frame_reported || !pixels || width == 0 || + height == 0 || stride < width * 4u || + height > byte_length / stride) + return; + alpha_pixels = 0; + colored_pixels = 0; + for (row = 0; row < height; row++) { + const unsigned char *source; + unsigned int column; + + source = pixels + row * stride; + for (column = 0; column < width; column++) { + const unsigned char *pixel; + + pixel = source + column * 4u; + if (pixel[3] != 0) + alpha_pixels++; + if (pixel[0] != 0 || pixel[1] != 0 || pixel[2] != 0) + colored_pixels++; + } + } + wsprintfW( + receipt, + L"PocketJS WM6 receipt: Rust pixels alpha=%lu color=%lu " + L"first BGRA=%02lx/%02lx/%02lx/%02lx\r\n", + alpha_pixels, + colored_pixels, + (DWORD)pixels[0], + (DWORD)pixels[1], + (DWORD)pixels[2], + (DWORD)pixels[3]); + OutputDebugString(receipt); +} + +static void report_successful_frame( + unsigned int width, + unsigned int height, + unsigned int stride, + unsigned int byte_length, + DWORD core_ms, + DWORD copy_ms, + DWORD present_ms) +{ + WCHAR receipt[256]; + DWORD now; + DWORD elapsed; + DWORD fps_tenths; + DWORD core_tenths; + DWORD copy_tenths; + DWORD present_tenths; + + now = GetTickCount(); + if (!g_first_frame_reported) { + wsprintfW( + receipt, + L"PocketJS WM6 receipt: Rust frame %lux%lu " + L"stride=%lu bytes=%lu\r\n", + (DWORD)width, + (DWORD)height, + (DWORD)stride, + (DWORD)byte_length); + OutputDebugString(receipt); + g_first_frame_reported = 1; + g_frame_window_started = now; + g_frame_window_count = 0; + g_profile_core_ms = 0; + g_profile_copy_ms = 0; + g_profile_present_ms = 0; + } + g_frame_window_count++; + g_profile_core_ms += core_ms; + g_profile_copy_ms += copy_ms; + g_profile_present_ms += present_ms; + elapsed = now - g_frame_window_started; + if (elapsed >= 2000u) { + fps_tenths = + (DWORD)((g_frame_window_count * 10000u) / elapsed); + core_tenths = + g_profile_core_ms * 10u / g_frame_window_count; + copy_tenths = + g_profile_copy_ms * 10u / g_frame_window_count; + present_tenths = + g_profile_present_ms * 10u / g_frame_window_count; + wsprintfW( + receipt, + L"PocketJS WM6 receipt: %lu.%lu FPS " + L"(%lu frames/%lu ms)\r\n", + fps_tenths / 10u, + fps_tenths % 10u, + (DWORD)g_frame_window_count, + elapsed); + OutputDebugString(receipt); + wsprintfW( + receipt, + L"PocketJS WM6 profile: core=%lu.%lu ms " + L"copy=%lu.%lu ms present=%lu.%lu ms/frame\r\n", + core_tenths / 10u, + core_tenths % 10u, + copy_tenths / 10u, + copy_tenths % 10u, + present_tenths / 10u, + present_tenths % 10u); + OutputDebugString(receipt); + g_frame_window_started = now; + g_frame_window_count = 0; + g_profile_core_ms = 0; + g_profile_copy_ms = 0; + g_profile_present_ms = 0; + } +} + +static int render_core_frame(void) +{ + const unsigned char *pixels; + unsigned int touches[1]; + unsigned int touch_count; + unsigned int frame_buttons; + unsigned int width; + unsigned int height; + unsigned int stride; + unsigned int byte_length; + char error[256]; + DWORD frame_started; + DWORD core_complete; + DWORD copy_complete; + DWORD present_complete; + + if (!g_quickjs_runtime || !g_quickjs_frame || !g_framebuffer_ready) + return 0; + frame_buttons = g_buttons | g_pressed_buttons; + g_pressed_buttons = 0; + touch_count = 0; + if (g_touch_active) { + touches[0] = 0x80000000u | + (((unsigned int)g_touch_y & 0x3ffu) << 10) | + ((unsigned int)g_touch_x & 0x3ffu); + touch_count = 1; + } + width = height = stride = byte_length = 0; + if (!g_first_frame_reported) + OutputDebugString( + L"PocketJS WM6 trace: host frame call begin\r\n"); + frame_started = GetTickCount(); + pixels = g_quickjs_frame( + g_quickjs_runtime, + frame_buttons, + touches, + touch_count, + &width, + &height, + &stride, + &byte_length, + error, + sizeof(error)); + if (!pixels) { + WCHAR message[256]; + + ascii_to_wide( + message, + 256, + error[0] ? error : "QuickJS/Rust frame returned no pixels"); + return stop_frame_rendering(message); + } + core_complete = GetTickCount(); + if (!g_first_frame_reported) + OutputDebugString( + L"PocketJS WM6 trace: host frame call complete\r\n"); + report_first_frame_pixels( + pixels, width, height, stride, byte_length); + if (!wm6_framebuffer_copy_argb( + pixels, width, height, stride, byte_length)) + return stop_frame_rendering( + L"Rust framebuffer geometry or byte length is invalid"); + copy_complete = GetTickCount(); + g_frame_available = 1; + if (!g_first_frame_reported) + OutputDebugString( + L"PocketJS WM6 trace: ARGB32 conversion complete\r\n"); + if (!wm6_framebuffer_present()) + return stop_frame_rendering( + L"WM6 could not present the Rust framebuffer"); + present_complete = GetTickCount(); + report_successful_frame( + width, + height, + stride, + byte_length, + core_complete - frame_started, + copy_complete - core_complete, + present_complete - copy_complete); + return 1; +} + +static LRESULT CALLBACK DemoWindowProc(HWND window, UINT message, + WPARAM wparam, LPARAM lparam) +{ + switch (message) { + case WM_PAINT: + { + PAINTSTRUCT paint; + HDC dc = BeginPaint(window, &paint); + int should_present; + + should_present = + g_framebuffer_ready && g_frame_available; + if (!should_present) + FillRect( + dc, &paint.rcPaint, + (HBRUSH)GetStockObject(BLACK_BRUSH)); + EndPaint(window, &paint); + /* + * Never lock the DirectDraw primary surface while a GDI paint DC + * is active. Some Windows CE display drivers serialize those two + * access paths and otherwise deadlock inside DirectDraw::Lock. + */ + if (should_present && !wm6_framebuffer_present()) { + g_framebuffer_ready = 0; + g_frame_available = 0; + } + } + return 0; + case WM_TIMER: + render_core_frame(); + return 0; + case WM_LBUTTONDOWN: + update_touch_position(lparam); + g_touch_active = 1; + SetCapture(window); + render_core_frame(); + return 0; + case WM_MOUSEMOVE: + if (g_touch_active) + update_touch_position(lparam); + return 0; + case WM_LBUTTONUP: + if (g_touch_active) { + update_touch_position(lparam); + render_core_frame(); + g_touch_active = 0; + ReleaseCapture(); + render_core_frame(); + } + return 0; + case WM_CAPTURECHANGED: + g_touch_active = 0; + return 0; + case WM_KEYDOWN: + { + unsigned int button; + + button = button_for_key(wparam); + if (button && !(g_buttons & button)) + g_pressed_buttons |= button; + g_buttons |= button; + } + if (wparam == VK_ESCAPE) { + DestroyWindow(window); + return 0; + } + return 0; + case WM_KEYUP: + g_buttons &= ~button_for_key(wparam); + return 0; + case WM_KILLFOCUS: + g_buttons = 0; + g_pressed_buttons = 0; + g_touch_active = 0; + if (GetCapture() == window) + ReleaseCapture(); + return 0; + case WM_DESTROY: + leave_fullscreen(window); + KillTimer(window, 1); + wm6_framebuffer_close(); + g_framebuffer_ready = 0; + g_frame_available = 0; + if (g_quickjs_destroy && g_quickjs_runtime) + g_quickjs_destroy(g_quickjs_runtime); + g_quickjs_runtime = NULL; + if (g_quickjs_module) + FreeLibrary(g_quickjs_module); + g_quickjs_module = NULL; + PostQuitMessage(0); + return 0; + } + return DefWindowProc(window, message, wparam, lparam); +} + +int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPWSTR command, int show) +{ + static const WCHAR class_name[] = L"PocketJSWM6Demo"; + HMODULE module; + wm6_qjs_abi_version_fn abi_version; + wm6_qjs_create_fn create_runtime; + wm6_qjs_set_pak_fn set_pak; + wm6_qjs_eval_fn eval; + wm6_qjs_drain_jobs_fn drain_jobs; + wm6_qjs_frame_fn frame; + wm6_qjs_destroy_fn destroy_runtime; + wm6_qjs_handle runtime; + char result[256]; + unsigned char *bundle; + unsigned char *pak; + unsigned int bundle_length; + unsigned int pak_length; + WCHAR create_error[256]; + WCHAR *message; + WNDCLASS window_class; + HWND window; + MSG message_loop; + int rotation_ready; + int render_scale; + int width_scale; + int height_scale; + int status; + unsigned int loaded_abi; + int display_height; + int display_width; + int viewport_height; + int viewport_width; + + (void)instance; + (void)previous; + (void)command; + (void)show; + + module = LoadLibrary(L"PocketJS.WM6.QuickJS.v3.dll"); + if (!module) { + MessageBox( + NULL, + L"PocketJS.WM6.QuickJS.v3.dll was not deployed or could not load", + L"PocketJS QuickJS Host", + MB_OK); + return 1; + } + abi_version = (wm6_qjs_abi_version_fn)GetProcAddress( + module, L"wm6_qjs_abi_version"); + create_runtime = (wm6_qjs_create_fn)GetProcAddress( + module, L"wm6_qjs_create"); + eval = (wm6_qjs_eval_fn)GetProcAddress(module, L"wm6_qjs_eval"); + drain_jobs = (wm6_qjs_drain_jobs_fn)GetProcAddress( + module, L"wm6_qjs_drain_jobs"); + set_pak = (wm6_qjs_set_pak_fn)GetProcAddress( + module, L"wm6_qjs_set_pak"); + frame = (wm6_qjs_frame_fn)GetProcAddress( + module, L"wm6_qjs_frame"); + destroy_runtime = (wm6_qjs_destroy_fn)GetProcAddress( + module, L"wm6_qjs_destroy"); + if (!abi_version || !create_runtime || !set_pak || !eval || + !drain_jobs || !frame || !destroy_runtime) { + FreeLibrary(module); + MessageBox(NULL, L"QuickJS ABI export missing", + L"PocketJS QuickJS Host", MB_OK); + return 2; + } + loaded_abi = abi_version(); + if (loaded_abi != WM6_QJS_ABI_VERSION) { + wsprintfW( + create_error, + L"QuickJS ABI mismatch: expected %lu, loaded %lu", + (DWORD)WM6_QJS_ABI_VERSION, + (DWORD)loaded_abi); + FreeLibrary(module); + MessageBox(NULL, create_error, + L"PocketJS QuickJS Host", MB_OK); + return 3; + } + + g_display_rotated = 0; + g_shell_hidden = 0; + g_taskbar_hidden = 0; + g_aygshell_module = NULL; + g_sh_fullscreen = NULL; + rotation_ready = rotate_display_90(); + display_width = GetSystemMetrics(SM_CXSCREEN); + display_height = GetSystemMetrics(SM_CYSCREEN); + width_scale = display_width / 320; + height_scale = display_height / 240; + render_scale = + width_scale < height_scale ? width_scale : height_scale; + if (render_scale < 1) + render_scale = 1; + viewport_width = display_width / render_scale; + viewport_height = display_height / render_scale; + g_display_width = display_width; + g_display_height = display_height; + g_viewport_width = viewport_width; + g_viewport_height = viewport_height; + + runtime = create_runtime( + 8u * 1024u * 1024u, + 256u * 1024u, + (unsigned int)viewport_width, + (unsigned int)viewport_height, + result, + sizeof(result)); + if (!runtime) { + restore_display_orientation(); + ascii_to_wide(create_error, 256, result); + FreeLibrary(module); + MessageBox(NULL, create_error, L"QuickJS create failed", MB_OK); + return 4; + } + bundle = read_neighbor_file( + L"PocketJS.WM6.Demo.js", &bundle_length); + pak = read_neighbor_file( + L"PocketJS.WM6.Demo.pak", &pak_length); + message = (WCHAR *)LocalAlloc(LMEM_FIXED, 1024 * sizeof(WCHAR)); + if (!bundle || !pak || !message) { + if (bundle) + LocalFree(bundle); + if (pak) + LocalFree(pak); + if (message) LocalFree(message); + destroy_runtime(runtime); + FreeLibrary(module); + restore_display_orientation(); + MessageBox(NULL, L"Demo bundle allocation failed", + L"PocketJS QuickJS Host", MB_OK); + return 5; + } + status = set_pak( + runtime, pak, pak_length, result, sizeof(result)); + if (status == 0) + status = eval(runtime, (const char *)bundle, bundle_length, + result, sizeof(result)); + if (status == 0) + status = drain_jobs(runtime, result, sizeof(result)) < 0 ? -1 : 0; + LocalFree(bundle); + LocalFree(pak); + + if (status != 0) { + restore_display_orientation(); + ascii_to_wide(message, 1024, result); + MessageBox(NULL, message, L"PocketJS QuickJS DLL failure", MB_OK); + destroy_runtime(runtime); + FreeLibrary(module); + LocalFree(message); + return 5; + } + g_buttons = 0; + g_pressed_buttons = 0; + g_touch_active = 0; + g_touch_x = 0; + g_touch_y = 0; + g_frame_error_shown = 0; + g_first_frame_reported = 0; + g_frame_window_started = 0; + g_frame_window_count = 0; + g_profile_core_ms = 0; + g_profile_copy_ms = 0; + g_profile_present_ms = 0; + g_quickjs_module = module; + g_quickjs_runtime = runtime; + g_quickjs_frame = frame; + g_quickjs_destroy = destroy_runtime; + if (rotation_ready) + OutputDebugString(L"PocketJS WM6: landscape display active\r\n"); + else + OutputDebugString(L"PocketJS WM6: display rotation unavailable\r\n"); + wsprintfW( + message, + L"PocketJS WM6 receipt: ABI v%lu, display=%lux%lu, " + L"viewport=%lux%lu, scale=%lux, " + L"bundle=%lu bytes, pak=%lu bytes\r\n", + (DWORD)WM6_QJS_ABI_VERSION, + (DWORD)display_width, + (DWORD)display_height, + (DWORD)viewport_width, + (DWORD)viewport_height, + (DWORD)render_scale, + (DWORD)bundle_length, + (DWORD)pak_length); + OutputDebugString(message); + memset(&window_class, 0, sizeof(window_class)); + window_class.lpfnWndProc = DemoWindowProc; + window_class.hInstance = instance; + window_class.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); + window_class.lpszClassName = class_name; + if (!RegisterClass(&window_class)) { + restore_display_orientation(); + destroy_runtime(runtime); + FreeLibrary(module); + g_quickjs_module = NULL; + g_quickjs_runtime = NULL; + LocalFree(message); + return 6; + } + window = CreateWindow(class_name, + rotation_ready + ? L"PocketJS Hero Demo [landscape]" + : L"PocketJS Hero Demo [rotation unavailable]", + WS_VISIBLE, 0, 0, + display_width, + display_height, + NULL, NULL, instance, NULL); + if (!window) { + restore_display_orientation(); + destroy_runtime(runtime); + FreeLibrary(module); + g_quickjs_module = NULL; + g_quickjs_runtime = NULL; + LocalFree(message); + return 7; + } + g_framebuffer_ready = 0; + g_frame_available = 0; + if (wm6_framebuffer_open( + window, viewport_width, viewport_height)) { + g_framebuffer_ready = 1; + OutputDebugString( + L"PocketJS WM6: Rust core ARGB32 presenter active\r\n"); + } else { + wm6_framebuffer_close(); + OutputDebugString(L"PocketJS WM6: DirectDraw unavailable\r\n"); + } + ShowWindow(window, show); + enter_fullscreen(window); + UpdateWindow(window); + if (g_framebuffer_ready) { + render_core_frame(); + SetTimer(window, 1, 16, NULL); + } + while (GetMessage(&message_loop, NULL, 0, 0)) { + TranslateMessage(&message_loop); + DispatchMessage(&message_loop); + } + restore_display_orientation(); + LocalFree(message); + return 0; +} diff --git a/hosts/wm6/vs2005/src/vapor_main.c b/hosts/wm6/vs2005/src/vapor_main.c new file mode 100644 index 000000000..f509aeb90 --- /dev/null +++ b/hosts/wm6/vs2005/src/vapor_main.c @@ -0,0 +1,344 @@ +/* Pocket Vapor AOT host for Windows Mobile 6 / Visual C++ 2005. + * + * The application logic and paint effects live in generated/todo.gba.c. + * This file owns the WM6 window, GDI presentation, and input mapping. + */ + +#include +#include +#include + +#define VP_GRID_W 30 +#define VP_GRID_H 20 + +#include "vapor.h" + +#define VP_BUTTON_A 0 +#define VP_BUTTON_B 1 +#define VP_BUTTON_SELECT 2 +#define VP_BUTTON_START 3 +#define VP_BUTTON_RIGHT 4 +#define VP_BUTTON_LEFT 5 +#define VP_BUTTON_UP 6 +#define VP_BUTTON_DOWN 7 + +u8 vp_grid_ch[VP_GRID_H][VP_GRID_W]; +u8 vp_grid_pal[VP_GRID_H][VP_GRID_W]; + +static const TCHAR kWindowClass[] = _T("PocketJS.WM6.Vapor"); +static HDC g_back_buffer; +static HBITMAP g_back_bitmap; +static HBITMAP g_back_previous_bitmap; +static int g_back_width; +static int g_back_height; + +static COLORREF color_from_rgb555(u16 color) +{ + int red = (color & 31) * 255 / 31; + int green = ((color >> 5) & 31) * 255 / 31; + int blue = ((color >> 10) & 31) * 255 / 31; + return RGB(red, green, blue); +} + +static void release_back_buffer(void) +{ + if (g_back_buffer != NULL && g_back_previous_bitmap != NULL) { + SelectObject(g_back_buffer, g_back_previous_bitmap); + } + if (g_back_bitmap != NULL) DeleteObject(g_back_bitmap); + if (g_back_buffer != NULL) DeleteDC(g_back_buffer); + g_back_buffer = NULL; + g_back_bitmap = NULL; + g_back_previous_bitmap = NULL; + g_back_width = 0; + g_back_height = 0; +} + +static BOOL ensure_back_buffer(HDC target, int width, int height) +{ + HDC next_buffer; + HBITMAP next_bitmap; + HBITMAP next_previous_bitmap; + + if ( + g_back_buffer != NULL && + g_back_width == width && + g_back_height == height + ) { + return TRUE; + } + + release_back_buffer(); + next_buffer = CreateCompatibleDC(target); + next_bitmap = CreateCompatibleBitmap(target, width, height); + if (next_buffer == NULL || next_bitmap == NULL) { + if (next_bitmap != NULL) DeleteObject(next_bitmap); + if (next_buffer != NULL) DeleteDC(next_buffer); + return FALSE; + } + next_previous_bitmap = (HBITMAP)SelectObject(next_buffer, next_bitmap); + if (next_previous_bitmap == NULL) { + DeleteObject(next_bitmap); + DeleteDC(next_buffer); + return FALSE; + } + + g_back_buffer = next_buffer; + g_back_bitmap = next_bitmap; + g_back_previous_bitmap = next_previous_bitmap; + g_back_width = width; + g_back_height = height; + return TRUE; +} + +static HFONT create_grid_font(int cell_height) +{ + const TCHAR face_name[] = _T("Courier New"); + LOGFONT description; + int index; + + ZeroMemory(&description, sizeof(description)); + description.lfHeight = -(cell_height * 3 / 4); + description.lfWeight = FW_BOLD; + description.lfCharSet = ANSI_CHARSET; + description.lfOutPrecision = OUT_DEFAULT_PRECIS; + description.lfClipPrecision = CLIP_DEFAULT_PRECIS; + description.lfQuality = DEFAULT_QUALITY; + description.lfPitchAndFamily = FIXED_PITCH | FF_MODERN; + for ( + index = 0; + index < LF_FACESIZE - 1 && face_name[index] != _T('\0'); + ++index + ) { + description.lfFaceName[index] = face_name[index]; + } + description.lfFaceName[index] = _T('\0'); + return CreateFontIndirect(&description); +} + +static void draw_grid(HWND window, HDC target) +{ + RECT client; + HDC back; + HFONT font; + HFONT previous_font; + int width; + int height; + int cell_width; + int cell_height; + int row; + + GetClientRect(window, &client); + width = client.right - client.left; + height = client.bottom - client.top; + if (width <= 0 || height <= 0) return; + if (!ensure_back_buffer(target, width, height)) return; + + back = g_back_buffer; + cell_width = width / VP_GRID_W; + cell_height = height / VP_GRID_H; + font = create_grid_font(cell_height); + previous_font = NULL; + if (font != NULL) previous_font = (HFONT)SelectObject(back, font); + SetBkMode(back, OPAQUE); + + for (row = 0; row < VP_GRID_H; ++row) { + TCHAR text[VP_GRID_W + 1]; + RECT row_rect; + u8 palette = vp_grid_pal[row][0]; + COLORREF ink; + COLORREF paper; + int column; + + if (palette >= vp_palette_count) palette = 0; + ink = color_from_rgb555(vp_palettes[(u16)palette * 16 + 1]); + paper = color_from_rgb555(vp_palettes[(u16)palette * 16 + 2]); + for (column = 0; column < VP_GRID_W; ++column) { + u8 ch = vp_grid_ch[row][column]; + text[column] = (TCHAR)((ch >= 0x20 && ch <= 0x7e) ? ch : ' '); + } + text[VP_GRID_W] = _T('\0'); + + row_rect.left = 0; + row_rect.top = row * cell_height; + row_rect.right = width; + row_rect.bottom = + row == VP_GRID_H - 1 ? height : (row + 1) * cell_height; + SetTextColor(back, ink); + SetBkColor(back, paper); + ExtTextOut( + back, + cell_width / 2, + row_rect.top + (cell_height / 8), + ETO_OPAQUE | ETO_CLIPPED, + &row_rect, + text, + VP_GRID_W, + NULL + ); + } + + BitBlt(target, 0, 0, width, height, back, 0, 0, SRCCOPY); + if (previous_font != NULL) SelectObject(back, previous_font); + if (font != NULL) DeleteObject(font); + vp_rows_dirty = 0; +} + +static int button_for_key(WPARAM key) +{ + switch (key) { + case VK_RETURN: + case 0x1c: + return VP_BUTTON_A; + case VK_BACK: + return VP_BUTTON_B; + case VK_UP: + return VP_BUTTON_UP; + case VK_DOWN: + return VP_BUTTON_DOWN; + case VK_LEFT: + return VP_BUTTON_LEFT; + case VK_RIGHT: + return VP_BUTTON_RIGHT; + case VK_F1: + return VP_BUTTON_SELECT; + case VK_F2: + return VP_BUTTON_START; + } + return -1; +} + +static void press_button(HWND window, u8 button) +{ + app_on_button(button); + if (app_flush()) InvalidateRect(window, NULL, FALSE); +} + +static LRESULT CALLBACK window_procedure( + HWND window, + UINT message, + WPARAM w_param, + LPARAM l_param +) +{ + switch (message) { + case WM_CREATE: + vp_row_clear(0, VP_GRID_H); + app_init(); + app_flush(); + InvalidateRect(window, NULL, FALSE); + return 0; + + case WM_KEYDOWN: + { + int button = button_for_key(w_param); + if (button >= 0) { + press_button(window, (u8)button); + } else if (w_param == VK_ESCAPE) { + DestroyWindow(window); + } + } + return 0; + + case WM_LBUTTONUP: + { + RECT client; + int x = (short)LOWORD(l_param); + int y = (short)HIWORD(l_param); + GetClientRect(window, &client); + if (y < client.bottom / 3) { + press_button(window, VP_BUTTON_UP); + } else if (y < (client.bottom * 2) / 3) { + press_button(window, VP_BUTTON_DOWN); + } else if (x < client.right / 2) { + press_button(window, VP_BUTTON_A); + } else { + press_button(window, VP_BUTTON_B); + } + } + return 0; + + case WM_SIZE: + release_back_buffer(); + InvalidateRect(window, NULL, FALSE); + return 0; + + case WM_ERASEBKGND: + return 1; + + case WM_PAINT: + { + PAINTSTRUCT paint; + HDC dc = BeginPaint(window, &paint); + draw_grid(window, dc); + EndPaint(window, &paint); + } + return 0; + + case WM_DESTROY: + release_back_buffer(); + PostQuitMessage(0); + return 0; + } + return DefWindowProc(window, message, w_param, l_param); +} + +int WINAPI WinMain( + HINSTANCE instance, + HINSTANCE previous_instance, + LPWSTR command_line, + int show_command +) +{ + WNDCLASS window_class; + HWND window; + MSG message; + BOOL message_result; + + (void)previous_instance; + (void)command_line; + ZeroMemory(&window_class, sizeof(window_class)); + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.lpfnWndProc = window_procedure; + window_class.hInstance = instance; + window_class.hCursor = LoadCursor(NULL, IDC_ARROW); + window_class.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); + window_class.lpszClassName = kWindowClass; + + if ( + !RegisterClass(&window_class) && + GetLastError() != ERROR_CLASS_ALREADY_EXISTS + ) { + return 1; + } + + SHInitExtraControls(); + window = CreateWindow( + kWindowClass, + _T("Pocket Vapor Todo"), + WS_VISIBLE, + 0, + 0, + GetSystemMetrics(SM_CXSCREEN), + GetSystemMetrics(SM_CYSCREEN), + NULL, + NULL, + instance, + NULL + ); + if (window == NULL) return 2; + + SHFullScreen( + window, + SHFS_HIDETASKBAR | SHFS_HIDESIPBUTTON | SHFS_HIDESTARTICON + ); + ShowWindow(window, show_command); + UpdateWindow(window); + + while ((message_result = GetMessage(&message, NULL, 0, 0)) > 0) { + TranslateMessage(&message); + DispatchMessage(&message); + } + if (message_result < 0) return 3; + return (int)message.wParam; +} diff --git a/hosts/wm6/vs2005/src/wm6_framebuffer.c b/hosts/wm6/vs2005/src/wm6_framebuffer.c new file mode 100644 index 000000000..2c5399530 --- /dev/null +++ b/hosts/wm6/vs2005/src/wm6_framebuffer.c @@ -0,0 +1,714 @@ +#include +#include + +#include "wm6_framebuffer.h" + +/* + * The WM6 Professional SDK's legacy ddraw.h omits this public DirectDraw + * capability spelling even though CreateSurface accepts its documented value. + */ +#ifndef DDSCAPS_OFFSCREENPLAIN +#define DDSCAPS_OFFSCREENPLAIN 0x00000040L +#endif + +static LPDIRECTDRAW g_direct_draw; +static LPDIRECTDRAWSURFACE g_primary; +static LPDIRECTDRAWSURFACE g_offscreen; +static unsigned short *g_pixels; +static DWORD *g_gdi_pixels; +static BITMAPINFO g_bitmap_info; +static HWND g_window; +static int g_width; +static int g_height; +static int g_surface_reported; +static int g_directdraw_disabled; +static int g_gdi_reported; +static DDPIXELFORMAT g_primary_format; +static int g_primary_format_known; +static const unsigned char *g_last_argb; +static unsigned int g_last_argb_width; +static unsigned int g_last_argb_height; +static unsigned int g_last_argb_stride; +static unsigned int g_last_argb_length; + +static unsigned short rgb565( + unsigned int red, + unsigned int green, + unsigned int blue) +{ + return (unsigned short)(((red & 0xf8u) << 8) | + ((green & 0xfcu) << 3) | + (blue >> 3)); +} + +static DWORD pack_component(unsigned int value, DWORD mask) +{ + DWORD shifted; + DWORD maximum; + int shift; + + if (!mask) + return 0; + shift = 0; + shifted = mask; + while ((shifted & 1u) == 0) { + shifted >>= 1; + shift++; + } + maximum = shifted; + return (((DWORD)value * maximum + 127u) / 255u) << shift; +} + +static DWORD convert_pixel( + unsigned short source, + const DDPIXELFORMAT *format) +{ + unsigned int red; + unsigned int green; + unsigned int blue; + + red = ((source >> 11) & 31u) * 255u / 31u; + green = ((source >> 5) & 63u) * 255u / 63u; + blue = (source & 31u) * 255u / 31u; + return pack_component(red, format->dwRBitMask) | + pack_component(green, format->dwGBitMask) | + pack_component(blue, format->dwBBitMask); +} + +static int resolve_surface_pixel_format( + LPDIRECTDRAWSURFACE target, + const DDSURFACEDESC *surface, + DDPIXELFORMAT *format) +{ + HRESULT status; + + memset(format, 0, sizeof(*format)); + format->dwSize = sizeof(*format); + status = target->lpVtbl->GetPixelFormat(target, format); + if (status != DD_OK || format->dwRGBBitCount == 0) + *format = surface->ddpfPixelFormat; + if (format->dwRGBBitCount == 16 && + (!format->dwRBitMask || !format->dwGBitMask || + !format->dwBBitMask)) { + format->dwRBitMask = 0xf800u; + format->dwGBitMask = 0x07e0u; + format->dwBBitMask = 0x001fu; + } else if ((format->dwRGBBitCount == 24 || + format->dwRGBBitCount == 32) && + (!format->dwRBitMask || !format->dwGBitMask || + !format->dwBBitMask)) { + format->dwRBitMask = 0x00ff0000u; + format->dwGBitMask = 0x0000ff00u; + format->dwBBitMask = 0x000000ffu; + } + format->dwSize = sizeof(*format); + format->dwFlags |= DDPF_RGB; + return (format->dwRGBBitCount == 16 || + format->dwRGBBitCount == 24 || + format->dwRGBBitCount == 32) && + format->dwRBitMask && format->dwGBitMask && + format->dwBBitMask; +} + +static void release_directdraw(void) +{ + if (g_offscreen) { + g_offscreen->lpVtbl->Release(g_offscreen); + g_offscreen = NULL; + } + if (g_primary) { + g_primary->lpVtbl->Release(g_primary); + g_primary = NULL; + } + if (g_direct_draw) { + g_direct_draw->lpVtbl->Release(g_direct_draw); + g_direct_draw = NULL; + } +} + +int wm6_framebuffer_open(HWND window, int logical_width, int logical_height) +{ + DDSURFACEDESC description; + DDSURFACEDESC primary_description; + DDPIXELFORMAT primary_format; + HRESULT status; + unsigned int pixel_count; + int have_primary_format; + + wm6_framebuffer_close(); + if (logical_width <= 0 || logical_height <= 0 || + logical_width > 2048 || logical_height > 2048) + return 0; + pixel_count = (unsigned int)logical_width * + (unsigned int)logical_height; + g_pixels = (unsigned short *)LocalAlloc( + LMEM_FIXED, pixel_count * sizeof(unsigned short)); + g_gdi_pixels = (DWORD *)LocalAlloc( + LMEM_FIXED, pixel_count * sizeof(DWORD)); + if (!g_pixels || !g_gdi_pixels) { + wm6_framebuffer_close(); + return 0; + } + memset(&g_bitmap_info, 0, sizeof(g_bitmap_info)); + g_bitmap_info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + g_bitmap_info.bmiHeader.biWidth = logical_width; + g_bitmap_info.bmiHeader.biHeight = logical_height; + g_bitmap_info.bmiHeader.biPlanes = 1; + g_bitmap_info.bmiHeader.biBitCount = 32; + g_bitmap_info.bmiHeader.biCompression = BI_RGB; + g_bitmap_info.bmiHeader.biSizeImage = + pixel_count * sizeof(DWORD); + g_window = window; + g_width = logical_width; + g_height = logical_height; + g_surface_reported = 0; + g_directdraw_disabled = 0; + g_gdi_reported = 0; + memset(&g_primary_format, 0, sizeof(g_primary_format)); + g_primary_format_known = 0; + + status = DirectDrawCreate(NULL, &g_direct_draw, NULL); + if (status != DD_OK || !g_direct_draw) { + g_directdraw_disabled = 1; + OutputDebugString( + L"PocketJS WM6: DirectDraw unavailable; using GDI DIB\r\n"); + release_directdraw(); + return 1; + } + status = g_direct_draw->lpVtbl->SetCooperativeLevel( + g_direct_draw, window, DDSCL_NORMAL); + if (status != DD_OK) { + g_directdraw_disabled = 1; + OutputDebugString( + L"PocketJS WM6: DirectDraw cooperative level failed; " + L"using GDI DIB\r\n"); + release_directdraw(); + return 1; + } + memset(&description, 0, sizeof(description)); + description.dwSize = sizeof(description); + description.dwFlags = DDSD_CAPS; + description.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE; + status = g_direct_draw->lpVtbl->CreateSurface( + g_direct_draw, &description, &g_primary, NULL); + if (status != DD_OK || !g_primary) { + g_directdraw_disabled = 1; + OutputDebugString( + L"PocketJS WM6: DirectDraw primary unavailable; " + L"using GDI DIB\r\n"); + release_directdraw(); + return 1; + } + memset(&primary_description, 0, sizeof(primary_description)); + primary_description.dwSize = sizeof(primary_description); + memset(&primary_format, 0, sizeof(primary_format)); + primary_format.dwSize = sizeof(primary_format); + status = g_primary->lpVtbl->GetSurfaceDesc( + g_primary, &primary_description); + have_primary_format = + status == DD_OK && + resolve_surface_pixel_format( + g_primary, &primary_description, &primary_format); + if (have_primary_format) { + g_primary_format = primary_format; + g_primary_format_known = 1; + } + memset(&description, 0, sizeof(description)); + description.dwSize = sizeof(description); + description.dwFlags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT; + description.dwWidth = (DWORD)logical_width; + description.dwHeight = (DWORD)logical_height; + description.ddsCaps.dwCaps = + DDSCAPS_OFFSCREENPLAIN | DDSCAPS_SYSTEMMEMORY; + status = g_direct_draw->lpVtbl->CreateSurface( + g_direct_draw, &description, &g_offscreen, NULL); + if (status != DD_OK || !g_offscreen) { + /* + * Some CE drivers choose the only lockable heap themselves and + * reject an explicit SYSTEMMEMORY request. + */ + if (g_offscreen) { + g_offscreen->lpVtbl->Release(g_offscreen); + g_offscreen = NULL; + } + description.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN; + status = g_direct_draw->lpVtbl->CreateSurface( + g_direct_draw, &description, &g_offscreen, NULL); + } + if (status != DD_OK || !g_offscreen) { + /* + * Some VGA CE drivers reject the OFFSCREENPLAIN capability exposed by + * newer DirectDraw headers, but accept a plain system-memory request. + */ + if (g_offscreen) { + g_offscreen->lpVtbl->Release(g_offscreen); + g_offscreen = NULL; + } + if (have_primary_format) { + description.dwFlags = + DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT | + DDSD_PIXELFORMAT; + description.ddsCaps.dwCaps = DDSCAPS_SYSTEMMEMORY; + description.ddpfPixelFormat = primary_format; + status = g_direct_draw->lpVtbl->CreateSurface( + g_direct_draw, &description, &g_offscreen, NULL); + } + } + if (status != DD_OK || !g_offscreen) { + WCHAR receipt[128]; + + wsprintfW( + receipt, + L"PocketJS WM6 DirectDraw: offscreen create failed " + L"hr=%08lx\r\n", + (DWORD)status); + OutputDebugString(receipt); + g_directdraw_disabled = 1; + release_directdraw(); + return 1; + } + return 1; +} + +void wm6_framebuffer_close(void) +{ + release_directdraw(); + if (g_pixels) { + LocalFree(g_pixels); + g_pixels = NULL; + } + if (g_gdi_pixels) { + LocalFree(g_gdi_pixels); + g_gdi_pixels = NULL; + } + memset(&g_bitmap_info, 0, sizeof(g_bitmap_info)); + g_window = NULL; + g_width = 0; + g_height = 0; + g_surface_reported = 0; + g_directdraw_disabled = 0; + g_gdi_reported = 0; + memset(&g_primary_format, 0, sizeof(g_primary_format)); + g_primary_format_known = 0; + g_last_argb = NULL; + g_last_argb_width = 0; + g_last_argb_height = 0; + g_last_argb_stride = 0; + g_last_argb_length = 0; +} + +static int copy_argb_to_gdi( + const unsigned char *pixels, + unsigned int width, + unsigned int height, + unsigned int stride, + unsigned int byte_length) +{ + unsigned int row; + + if (!pixels || !g_gdi_pixels || + width != (unsigned int)g_width || + height != (unsigned int)g_height || stride < width * 4u || + height > byte_length / stride) + return 0; + /* + * A PocketJS ARGB32 row is BGRA in little-endian memory, which is already + * the byte order expected by a 32-bit BI_RGB DIB. Keep the bottom-up row + * order required by the positive DIB height. + */ + for (row = 0; row < height; row++) { + const unsigned char *source; + DWORD *gdi_destination; + + source = pixels + row * stride; + gdi_destination = + g_gdi_pixels + (height - row - 1u) * width; + memcpy(gdi_destination, source, width * 4u); + } + return 1; +} + +int wm6_framebuffer_copy_argb( + const unsigned char *pixels, + unsigned int width, + unsigned int height, + unsigned int stride, + unsigned int byte_length) +{ + unsigned int row; + + if (!pixels || !g_pixels || + width != (unsigned int)g_width || + height != (unsigned int)g_height || stride < width * 4u || + height > byte_length / stride) + return 0; + g_last_argb = pixels; + g_last_argb_width = width; + g_last_argb_height = height; + g_last_argb_stride = stride; + g_last_argb_length = byte_length; + if (g_directdraw_disabled) + return copy_argb_to_gdi( + pixels, width, height, stride, byte_length); + for (row = 0; row < height; row++) { + const unsigned char *source; + unsigned short *destination; + unsigned int column; + + source = pixels + row * stride; + destination = g_pixels + row * width; + for (column = 0; column < width; column++) { + /* PocketJS exposes little-endian ARGB32: B, G, R, A bytes. */ + destination[column] = rgb565( + source[column * 4u + 2u], + source[column * 4u + 1u], + source[column * 4u]); + } + } + return 1; +} + +static int present_directdraw(void) +{ + DDSURFACEDESC offscreen; + DDSURFACEDESC primary; + DDPIXELFORMAT pixel_format; + RECT source_rect; + RECT destination_rect; + HRESULT status; + int destination_width; + int destination_height; + int primary_width; + int primary_height; + int bytes_per_pixel; + int pitch; + int y; + + if (!g_primary || !g_offscreen || !g_pixels || + g_width <= 0 || g_height <= 0) + return 0; + memset(&offscreen, 0, sizeof(offscreen)); + offscreen.dwSize = sizeof(offscreen); + if (!g_surface_reported) + OutputDebugString( + L"PocketJS WM6 trace: DirectDraw offscreen lock begin\r\n"); + status = g_offscreen->lpVtbl->Lock( + g_offscreen, NULL, &offscreen, 0, NULL); + if (status == DDERR_SURFACELOST) { + g_offscreen->lpVtbl->Restore(g_offscreen); + status = g_offscreen->lpVtbl->Lock( + g_offscreen, NULL, &offscreen, 0, NULL); + } + if (status != DD_OK || !offscreen.lpSurface) { + WCHAR receipt[128]; + + wsprintfW( + receipt, + L"PocketJS WM6 DirectDraw: offscreen lock failed " + L"hr=%08lx\r\n", + (DWORD)status); + OutputDebugString(receipt); + return 0; + } + if (!resolve_surface_pixel_format( + g_offscreen, &offscreen, &pixel_format)) { + if (g_primary_format_known) { + pixel_format = g_primary_format; + } else { + OutputDebugString( + L"PocketJS WM6 DirectDraw: offscreen pixel format " + L"unavailable\r\n"); + g_offscreen->lpVtbl->Unlock( + g_offscreen, offscreen.lpSurface); + return 0; + } + } + /* + * A few CE drivers only populate lpSurface and lPitch in the descriptor + * returned by Lock. The dimensions were already validated at creation. + */ + if ((offscreen.dwWidth != 0 && + (int)offscreen.dwWidth != g_width) || + (offscreen.dwHeight != 0 && + (int)offscreen.dwHeight != g_height)) { + WCHAR receipt[160]; + + wsprintfW( + receipt, + L"PocketJS WM6 DirectDraw: offscreen geometry " + L"%lux%lu expected %ldx%ld\r\n", + offscreen.dwWidth, + offscreen.dwHeight, + (LONG)g_width, + (LONG)g_height); + OutputDebugString(receipt); + g_offscreen->lpVtbl->Unlock( + g_offscreen, offscreen.lpSurface); + return 0; + } + bytes_per_pixel = (int)pixel_format.dwRGBBitCount / 8; + if (bytes_per_pixel != 2 && bytes_per_pixel != 3 && + bytes_per_pixel != 4) { + OutputDebugString( + L"PocketJS WM6 DirectDraw: unsupported offscreen " + L"pixel depth\r\n"); + g_offscreen->lpVtbl->Unlock( + g_offscreen, offscreen.lpSurface); + return 0; + } + pitch = offscreen.lPitch < 0 + ? -offscreen.lPitch + : offscreen.lPitch; + if (g_width * bytes_per_pixel > pitch) { + WCHAR receipt[160]; + + wsprintfW( + receipt, + L"PocketJS WM6 DirectDraw: offscreen pitch=%ld " + L"requires=%ld\r\n", + (LONG)offscreen.lPitch, + (LONG)(g_width * bytes_per_pixel)); + OutputDebugString(receipt); + g_offscreen->lpVtbl->Unlock( + g_offscreen, offscreen.lpSurface); + return 0; + } + for (y = 0; y < g_height; y++) { + unsigned char *destination; + const unsigned short *source; + int x; + + destination = (unsigned char *)offscreen.lpSurface + + y * offscreen.lPitch; + source = &g_pixels[y * g_width]; + if (pixel_format.dwRGBBitCount == 16) { + unsigned short *output; + + output = (unsigned short *)destination; + if (pixel_format.dwRBitMask == 0xf800u && + pixel_format.dwGBitMask == 0x07e0u && + pixel_format.dwBBitMask == 0x001fu) { + memcpy( + output, + source, + (unsigned int)g_width * + sizeof(unsigned short)); + } else { + for (x = 0; x < g_width; x++) + output[x] = (unsigned short)convert_pixel( + source[x], &pixel_format); + } + } else if (pixel_format.dwRGBBitCount == 32) { + DWORD *output; + + output = (DWORD *)destination; + for (x = 0; x < g_width; x++) + output[x] = convert_pixel( + source[x], &pixel_format); + } else { + unsigned char *output; + + output = destination; + for (x = 0; x < g_width; x++) { + DWORD color; + + color = convert_pixel(source[x], &pixel_format); + output[x * 3] = (unsigned char)color; + output[x * 3 + 1] = (unsigned char)(color >> 8); + output[x * 3 + 2] = (unsigned char)(color >> 16); + } + } + } + status = g_offscreen->lpVtbl->Unlock( + g_offscreen, offscreen.lpSurface); + if (status != DD_OK) { + OutputDebugString( + L"PocketJS WM6 DirectDraw: offscreen unlock failed\r\n"); + return 0; + } + + memset(&primary, 0, sizeof(primary)); + primary.dwSize = sizeof(primary); + status = g_primary->lpVtbl->GetSurfaceDesc(g_primary, &primary); + if (status == DDERR_SURFACELOST) { + g_primary->lpVtbl->Restore(g_primary); + status = g_primary->lpVtbl->GetSurfaceDesc( + g_primary, &primary); + } + primary_width = (int)primary.dwWidth; + primary_height = (int)primary.dwHeight; + if (status != DD_OK || + primary_width <= 0 || primary_height <= 0) { + OutputDebugString( + L"PocketJS WM6 DirectDraw: primary description " + L"unavailable\r\n"); + return 0; + } + if (primary_width * g_height <= primary_height * g_width) { + destination_width = primary_width; + destination_height = g_height * primary_width / g_width; + } else { + destination_height = primary_height; + destination_width = g_width * primary_height / g_height; + } + destination_rect.left = + (primary_width - destination_width) / 2; + destination_rect.top = + (primary_height - destination_height) / 2; + destination_rect.right = + destination_rect.left + destination_width; + destination_rect.bottom = + destination_rect.top + destination_height; + source_rect.left = 0; + source_rect.top = 0; + source_rect.right = g_width; + source_rect.bottom = g_height; + + status = g_primary->lpVtbl->Blt( + g_primary, + &destination_rect, + g_offscreen, + &source_rect, + 0, + NULL); + if (status == DDERR_SURFACELOST) { + g_primary->lpVtbl->Restore(g_primary); + status = g_primary->lpVtbl->Blt( + g_primary, + &destination_rect, + g_offscreen, + &source_rect, + 0, + NULL); + } + if (status != DD_OK) { + WCHAR receipt[128]; + + wsprintfW( + receipt, + L"PocketJS WM6 DirectDraw: primary Blt failed " + L"hr=%08lx\r\n", + (DWORD)status); + OutputDebugString(receipt); + return 0; + } + if (!g_surface_reported) { + WCHAR receipt[256]; + + wsprintfW( + receipt, + L"PocketJS WM6 receipt: DirectDraw offscreen=%ldx%ld " + L"pitch=%ld rgb=%lu masks=%08lx/%08lx/%08lx " + L"primary=%ldx%ld\r\n", + (LONG)offscreen.dwWidth, + (LONG)offscreen.dwHeight, + (LONG)offscreen.lPitch, + pixel_format.dwRGBBitCount, + pixel_format.dwRBitMask, + pixel_format.dwGBitMask, + pixel_format.dwBBitMask, + (LONG)primary_width, + (LONG)primary_height); + OutputDebugString(receipt); + g_surface_reported = 1; + } + return 1; +} + +static int present_gdi(void) +{ + HDC dc; + RECT client; + int client_width; + int client_height; + int destination_width; + int destination_height; + int offset_x; + int offset_y; + int status; + + if (!g_window || !g_gdi_pixels || + g_width <= 0 || g_height <= 0) + return 0; + if (!GetClientRect(g_window, &client)) + return 0; + client_width = client.right - client.left; + client_height = client.bottom - client.top; + if (client_width <= 0 || client_height <= 0) + return 0; + if (client_width * g_height <= client_height * g_width) { + destination_width = client_width; + destination_height = + g_height * client_width / g_width; + } else { + destination_height = client_height; + destination_width = + g_width * client_height / g_height; + } + offset_x = (client_width - destination_width) / 2; + offset_y = (client_height - destination_height) / 2; + dc = GetDC(g_window); + if (!dc) + return 0; + status = StretchDIBits( + dc, + offset_x, + offset_y, + destination_width, + destination_height, + 0, + 0, + g_width, + g_height, + g_gdi_pixels, + &g_bitmap_info, + DIB_RGB_COLORS, + SRCCOPY); + ReleaseDC(g_window, dc); + if (!status) { + WCHAR receipt[128]; + + wsprintfW( + receipt, + L"PocketJS WM6 GDI: StretchDIBits failed error=%lu\r\n", + GetLastError()); + OutputDebugString(receipt); + return 0; + } + if (!g_gdi_reported) { + WCHAR receipt[160]; + + wsprintfW( + receipt, + L"PocketJS WM6 receipt: GDI DIB fallback=%ldx%ld " + L"client=%ldx%ld\r\n", + (LONG)g_width, + (LONG)g_height, + (LONG)client_width, + (LONG)client_height); + OutputDebugString(receipt); + g_gdi_reported = 1; + } + return 1; +} + +int wm6_framebuffer_present(void) +{ + if (!g_directdraw_disabled) { + if (present_directdraw()) + return 1; + g_directdraw_disabled = 1; + release_directdraw(); + OutputDebugString( + L"PocketJS WM6: DirectDraw presentation failed; " + L"using GDI DIB\r\n"); + if (!copy_argb_to_gdi( + g_last_argb, + g_last_argb_width, + g_last_argb_height, + g_last_argb_stride, + g_last_argb_length)) + return 0; + } + return present_gdi(); +} diff --git a/hosts/wm6/vs2005/src/wm6_framebuffer.h b/hosts/wm6/vs2005/src/wm6_framebuffer.h new file mode 100644 index 000000000..8f23732af --- /dev/null +++ b/hosts/wm6/vs2005/src/wm6_framebuffer.h @@ -0,0 +1,16 @@ +#ifndef POCKETJS_WM6_FRAMEBUFFER_H +#define POCKETJS_WM6_FRAMEBUFFER_H + +#include + +int wm6_framebuffer_open(HWND window, int logical_width, int logical_height); +void wm6_framebuffer_close(void); +int wm6_framebuffer_copy_argb( + const unsigned char *pixels, + unsigned int width, + unsigned int height, + unsigned int stride, + unsigned int byte_length); +int wm6_framebuffer_present(void); + +#endif diff --git a/tests/wm6-project.test.ts b/tests/wm6-project.test.ts new file mode 100644 index 000000000..6b1dda3d0 --- /dev/null +++ b/tests/wm6-project.test.ts @@ -0,0 +1,155 @@ +import { describe, expect, test } from "bun:test"; +import { readFile } from "node:fs/promises"; + +const root = new URL("../hosts/wm6/vs2005/", import.meta.url); + +describe("Windows Mobile 6 VS2005 projects", () => { + test("uses the Professional SDK ARMV4I smart-device platform", async () => { + const [solution, project, resource] = await Promise.all([ + readFile(new URL("PocketJS.WM6.sln", root), "utf8"), + readFile(new URL("PocketJS.WM6.Probe.vcproj", root), "utf8"), + readFile(new URL("resources/probe.rc", root), "utf8"), + ]); + + expect(solution).toContain("Microsoft Visual Studio Solution File, Format Version 9.00"); + expect(solution).toContain("Windows Mobile 6 Professional SDK (ARMV4I)"); + expect(project).toContain('Version="8.00"'); + expect(project).toContain('Name="Windows Mobile 6 Professional SDK (ARMV4I)"'); + expect(project).toContain("/subsystem:windowsce,5.02"); + expect(project).toContain("aygshell.lib coredll.lib"); + expect(project.match(/DisableSpecificWarnings="4201"/g)).toHaveLength(2); + expect(project.match(/TargetMachine="0"/g)).toHaveLength(2); + expect(project).not.toContain('TargetMachine="1"'); + expect( + project.match( + /RemoteDirectory="%CSIDL_PROGRAM_FILES%\\PocketJS\.WM6\.Probe"/g, + ), + ).toHaveLength(2); + expect( + project.match( + /RemoteExecutable="%CSIDL_PROGRAM_FILES%\\PocketJS\.WM6\.Probe\\PocketJS\.WM6\.Probe\.exe"/g, + ), + ).toHaveLength(2); + expect(project).not.toContain("Windows Mobile 6 Standard SDK"); + expect(project).toContain('RelativePath=".\\resources\\probe.rc"'); + expect(resource).toMatch(/\bHI_RES_AWARE\s+CEUX\s+\{\s*1\s*\}/); + }); + + test("keeps the probe compatible with the VC8 compiler", async () => { + const source = await readFile(new URL("src/main.cpp", root), "utf8"); + + expect(source).toContain("int WINAPI WinMain"); + expect(source).toContain("SHFullScreen"); + expect(source).toContain("WM_LBUTTONDOWN"); + expect(source).toContain("WM_KEYDOWN"); + expect(source).toContain("CreateCompatibleBitmap"); + expect(source).toContain("CreateFontIndirect"); + expect(source).not.toMatch(/\bCreateFont\s*\(/); + expect(source).not.toContain("lstrcpyn"); + expect(source).not.toMatch(/\b(auto|nullptr|constexpr|override)\b/); + }); + + test("includes a separate VC8 Pocket Vapor AOT application", async () => { + const [solution, project, host, generated, runtime] = await Promise.all([ + readFile(new URL("PocketJS.WM6.sln", root), "utf8"), + readFile(new URL("PocketJS.WM6.Vapor.vcproj", root), "utf8"), + readFile(new URL("src/vapor_main.c", root), "utf8"), + readFile(new URL("generated/todo.gba.c", root), "utf8"), + readFile(new URL("runtime/vapor_core.c", root), "utf8"), + ]); + + expect(solution).toContain('"PocketJS.WM6.Vapor"'); + expect(solution).toContain("{970E99AC-1918-451D-A515-9C23D9C3AC65}"); + expect(project).toContain('Version="8.00"'); + expect(project).toContain('Name="Windows Mobile 6 Professional SDK (ARMV4I)"'); + expect(project).toContain('RelativePath=".\\src\\vapor_main.c"'); + expect(project).toContain('RelativePath=".\\generated\\todo.gba.c"'); + expect(project).toContain( + 'RelativePath=".\\runtime\\vapor_core.c"', + ); + expect(project.match(/AdditionalIncludeDirectories="\.\\runtime"/g)).toHaveLength( + 2, + ); + expect( + project.match(/DisableSpecificWarnings="4201;4115;4214;4819"/g), + ).toHaveLength(2); + expect(project.match(/inline=__inline/g)).toHaveLength(2); + expect(project.match(/TargetMachine="0"/g)).toHaveLength(2); + expect( + project.match( + /RemoteExecutable="%CSIDL_PROGRAM_FILES%\\PocketJS\.WM6\.Vapor\\PocketJS\.WM6\.Vapor\.exe"/g, + ), + ).toHaveLength(2); + + expect(host).toContain("app_init()"); + expect(host).toContain("app_on_button(button)"); + expect(host).toContain("ExtTextOut"); + expect(host).toContain("WM_LBUTTONUP"); + expect(generated).toContain( + "GENERATED by vapor/compiler/compile.ts. DO NOT EDIT.", + ); + expect(generated).toContain("const char vp_app_title[] = \"VAPOR TODO\""); + expect(runtime).toContain("void vp_row_clear"); + }); + + test("keeps the QuickJS ABI3 deployment self-contained", async () => { + const [ + solution, + project, + host, + framebuffer, + abi, + framebufferHeader, + resource, + readme, + quickjsReadme, + demo, + demoPak, + executable, + dll, + ] = + await Promise.all([ + readFile(new URL("PocketJS.WM6.sln", root), "utf8"), + readFile(new URL("PocketJS.WM6.QuickJS.vcproj", root), "utf8"), + readFile(new URL("src/quickjs_deploy.c", root), "utf8"), + readFile(new URL("src/wm6_framebuffer.c", root), "utf8"), + readFile(new URL("runtime/wm6_quickjs_abi.h", root), "utf8"), + readFile(new URL("src/wm6_framebuffer.h", root), "utf8"), + readFile(new URL("resources/probe.rc", root), "utf8"), + readFile(new URL("README.md", root), "utf8"), + readFile(new URL("../quickjs/README.md", root), "utf8"), + readFile(new URL("prebuilt/PocketJS.WM6.Demo.js", root), "utf8"), + readFile(new URL("prebuilt/PocketJS.WM6.Demo.pak", root)), + readFile(new URL("prebuilt/PocketJS.WM6.QuickJS.Probe.exe", root)), + readFile(new URL("prebuilt/PocketJS.WM6.QuickJS.v3.dll", root)), + ]); + + expect(solution).toContain('"PocketJS.WM6.QuickJS"'); + expect(project).toContain('Version="8.00"'); + expect(project).toContain('Name="Windows Mobile 6 Professional SDK (ARMV4I)"'); + for (const path of [ + ".\\src\\quickjs_deploy.c", + ".\\src\\wm6_framebuffer.c", + ".\\runtime\\wm6_quickjs_abi.h", + ".\\src\\wm6_framebuffer.h", + ]) expect(project).toContain(`RelativePath="${path}"`); + expect(project).toContain("PocketJS.WM6.QuickJS.v3.dll"); + expect(project).toContain("PocketJS.WM6.Demo.js"); + expect(project).toContain("PocketJS.WM6.Demo.pak"); + expect(project.match(/TargetMachine="0"/g)).toHaveLength(2); + expect(project.match(/AdditionalDependencies="ddraw\.lib coredll\.lib"/g)).toHaveLength(2); + expect(host).toContain("wm6_qjs_frame_fn"); + expect(host).toContain("wm6_qjs_set_pak_fn"); + expect(framebuffer).toContain("#include "); + expect(framebuffer).toContain("wm6_framebuffer_present"); + expect(abi).toContain("#define WM6_QJS_ABI_VERSION 3u"); + for (const binary of [executable, dll]) { + expect(binary.subarray(0, 2).toString("ascii")).toBe("MZ"); + const peOffset = binary.readUInt32LE(0x3c); + expect(binary.subarray(peOffset, peOffset + 4).toString("binary")).toBe("PE\u0000\u0000"); + expect(binary.readUInt16LE(peOffset + 4)).toBe(0x01c0); + expect(binary.readUInt16LE(peOffset + 24 + 68)).toBe(9); + } + expect(dll.readUInt16LE(dll.readUInt32LE(0x3c) + 22) & 0x2000).toBe(0x2000); + }); +});