Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
tools/symbian/container/patches/*.patch whitespace=-trailing-space,-space-before-tab
tools/nspire/*.patch whitespace=-trailing-space,-space-before-tab
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ framework/src/styles.generated.ts
apps/launcher/covers/
# built wasm backend (regenerated by `bun tools/wasm.ts`)
hosts/web/pocketjs.wasm
# Ndless host objects, embedded guest source, link map, and executable.
hosts/nspire/build/
# generated per-demo XMB metadata (tools/psp.ts, from apps/<app>/psp/Psp.toml)
hosts/psp/Psp.toml
# Obsolete checkout-local SDK location; bootstrap uses the shared cache.
Expand Down
73 changes: 67 additions & 6 deletions hosts/iphone2g/pocket_runtime.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ extern void pocket_host_boot_stage(int stage);
#define REPORT_BOOT_STAGE(stage) ((void)(stage))
#endif

#ifndef POCKET_RUNTIME_JS_STACK_SIZE
#define POCKET_RUNTIME_JS_STACK_SIZE (256 * 1024)
#endif

typedef enum {
HostCreateNode,
HostDestroyNode,
Expand Down Expand Up @@ -94,6 +98,34 @@ static void take_exception(JSContext *exception_context) {
} else {
set_error("QuickJS exception");
}
#if defined(POCKET_RUNTIME_INCLUDE_EXCEPTION_STACK)
JSValue stack = JS_GetPropertyStr(exception_context, exception, "stack");
if (!JS_IsException(stack) && !JS_IsUndefined(stack) && !JS_IsNull(stack)) {
size_t stack_length = 0;
const char *stack_text = JS_ToCStringLen2(
exception_context,
&stack_length,
stack,
0
);
if (stack_text != 0 && stack_length > 0) {
size_t used = strlen(last_error);
size_t available = sizeof(last_error) - used - 1;
if (available > 0) {
last_error[used++] = '\n';
available -= 1;
size_t copy_length = stack_length < available ? stack_length : available;
memcpy(last_error + used, stack_text, copy_length);
last_error[used + copy_length] = '\0';
}
JS_FreeCString(exception_context, stack_text);
}
} else if (JS_IsException(stack)) {
JSValue stack_error = JS_GetException(exception_context);
JS_FreeValue(exception_context, stack_error);
}
JS_FreeValue(exception_context, stack);
#endif
JS_FreeValue(exception_context, exception);
}

Expand Down Expand Up @@ -561,7 +593,7 @@ int pocket_runtime_boot(
return 0;
}
REPORT_BOOT_STAGE(4);
JS_SetMaxStackSize(runtime, 256 * 1024);
JS_SetMaxStackSize(runtime, POCKET_RUNTIME_JS_STACK_SIZE);
context = JS_NewContext(runtime);
if (context == 0) {
set_error("QuickJS context allocation failed");
Expand Down Expand Up @@ -632,6 +664,7 @@ int pocket_runtime_boot(
/* One guest turn (frame call + job drain), then `tick_count` core ticks. */
static int run_frame(
uint32_t buttons,
uint32_t analog,
const PocketRuntimeContact *contacts,
unsigned int contact_count,
unsigned int tick_count
Expand Down Expand Up @@ -680,7 +713,7 @@ static int run_frame(
}
JSValue arguments[4] = {
JS_NewUint32(context, buttons),
JS_NewInt32(context, POCKET_ANALOG_CENTER),
JS_NewUint32(context, analog),
touch_array,
hit_array,
};
Expand Down Expand Up @@ -728,20 +761,48 @@ int pocket_runtime_tick(const PocketRuntimeInput *input) {
input->touch_y,
input->touch_hit
);
return run_frame(input->buttons, &contact, count, 1);
return run_frame(input->buttons, POCKET_ANALOG_CENTER, &contact, count, 1);
}

int pocket_runtime_tick_analog(uint32_t buttons, uint32_t analog) {
return run_frame(buttons, analog, 0, 0, 1);
}

int pocket_runtime_set_diagnostic_text(const char *text) {
if (runtime == 0 || context == 0 || runtime_failed || JS_IsUndefined(global)) return 0;
JSValue value = JS_NewString(context, text == 0 ? "" : text);
if (JS_IsException(value) ||
JS_SetPropertyStr(context, global, "__pocketInputDiagnostic", value) < 0) {
take_exception(context);
runtime_failed = 1;
return 0;
}
return 1;
}

int pocket_runtime_tick_contacts(const PocketRuntimeContactsInput *input) {
if (input == 0) return 0;
return run_frame(input->buttons, input->contacts, input->contact_count, 1);
return run_frame(
input->buttons,
POCKET_ANALOG_CENTER,
input->contacts,
input->contact_count,
1
);
}

int pocket_runtime_frame_contacts(
const PocketRuntimeContactsInput *input,
unsigned int tick_count
) {
if (input == 0) return 0;
return run_frame(input->buttons, input->contacts, input->contact_count, tick_count);
return run_frame(
input->buttons,
POCKET_ANALOG_CENTER,
input->contacts,
input->contact_count,
tick_count
);
}

int pocket_runtime_frame_ticks(
Expand All @@ -753,7 +814,7 @@ int pocket_runtime_frame_ticks(
) {
PocketRuntimeContact contact;
unsigned int count = single_contact(&contact, touch_down, touch_x, touch_y, touch_hit);
return run_frame(0, &contact, count, tick_count);
return run_frame(0, POCKET_ANALOG_CENTER, &contact, count, tick_count);
}

int pocket_runtime_frame(int touch_down, int touch_x, int touch_y, int touch_hit) {
Expand Down
4 changes: 4 additions & 0 deletions hosts/iphone2g/pocket_runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ typedef struct {
int touch_hit;
} PocketRuntimeInput;
int pocket_runtime_tick(const PocketRuntimeInput *input);
/* Button + analog-only entry for hosts with a stick/trackpad and no touch. */
int pocket_runtime_tick_analog(uint32_t buttons, uint32_t analog);
/* Optional host-acceptance text, exposed outside the application input ABI. */
int pocket_runtime_set_diagnostic_text(const char *text);

/*
* Multi-contact frame entry. `id` is the host's contact slot (0-255, stable
Expand Down
87 changes: 87 additions & 0 deletions hosts/nspire/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
REPO_ROOT := ../..
BUILD_DIR := build
DIST_DIR ?= $(REPO_ROOT)/dist/nspire
TARGET_JSON := $(CURDIR)/targets/armv5te-nspire-eabi.json
CORE_MANIFEST := $(REPO_ROOT)/engine/symbian/Cargo.toml
CORE_ARCHIVE := $(REPO_ROOT)/engine/symbian/target/armv5te-nspire-eabi/release/libpocketjs_symbian_core.a
APP_DATA := $(BUILD_DIR)/app_data.c

GCC := nspire-gcc
LD := nspire-ld
GENZEHN := genzehn
MAKE_PRG := make-prg

ifeq ($(QUICKJS_DIR),)
ifneq ($(MAKECMDGOALS),clean)
$(error QUICKJS_DIR is required and must contain libquickjs-sys/embed/quickjs)
endif
endif
QJS_SRC := $(QUICKJS_DIR)/libquickjs-sys/embed/quickjs
QJS_NAMES := quickjs cutils libregexp libunicode dtoa
QJS_OBJS := $(addprefix $(BUILD_DIR)/qjs/,$(addsuffix .o,$(QJS_NAMES)))
HOST_OBJS := $(BUILD_DIR)/main.o $(BUILD_DIR)/input.o $(BUILD_DIR)/framebuffer.o \
$(BUILD_DIR)/compat.o \
$(BUILD_DIR)/pocket_runtime.o $(BUILD_DIR)/app_data.o

COMMON_CFLAGS := -std=gnu11 -Os -marm -ffunction-sections -fdata-sections \
-fno-strict-aliasing -funsigned-char -Wall -Wextra \
-I. -I../iphone2g -I$(QJS_SRC) \
-DPOCKETJS_TARGET_ID='"nspire-cx2-dev"' -DPOCKETJS_HOST_ABI=9 \
-DPOCKET_RASTER_DENSITY=1 -DPOCKET_RUNTIME_REPORT_BOOT_STAGE=1 \
-DPOCKET_RUNTIME_INCLUDE_EXCEPTION_STACK=1 \
-DPOCKET_RUNTIME_JS_STACK_SIZE=65536 \
-DPOCKETJS_NO_MALLOC_USABLE_SIZE=1
QJS_CFLAGS := $(COMMON_CFLAGS) -D_GNU_SOURCE \
-DPOCKETJS_NO_ATOMICS=1 -DPOCKETJS_FIXED_TIMEZONE=1 \
-DCONFIG_VERSION='"pocket-nspire-cx2"' -D__TM_GMTOFF=tm_gmtoff \
-include malloc.h -Wno-sign-compare -Wno-unused-parameter
LDFLAGS := -Wl,--gc-sections -Wl,-Map,$(BUILD_DIR)/pocketjs-cx2.map

.PHONY: all clean core check-app-data
all: $(DIST_DIR)/pocketjs-cx2.tns

check-app-data:
@test -f $(APP_DATA) || (echo "missing $(APP_DATA); run bun nspire bundle"; exit 1)

core:
cd $(REPO_ROOT)/engine/symbian && cargo build --release --locked \
--no-default-features --features software-only,boot-stage \
--target $(TARGET_JSON) -Z json-target-spec \
-Z build-std=core,alloc,compiler_builtins \
-Z build-std-features=compiler-builtins-mem

$(BUILD_DIR) $(BUILD_DIR)/qjs $(DIST_DIR):
mkdir -p $@

$(BUILD_DIR)/main.o: main.c | $(BUILD_DIR)
$(GCC) $(COMMON_CFLAGS) -c $< -o $@

$(BUILD_DIR)/input.o: input.c | $(BUILD_DIR)
$(GCC) $(COMMON_CFLAGS) -c $< -o $@

$(BUILD_DIR)/framebuffer.o: framebuffer.c | $(BUILD_DIR)
$(GCC) $(COMMON_CFLAGS) -c $< -o $@

$(BUILD_DIR)/compat.o: compat.c | $(BUILD_DIR)
$(GCC) $(COMMON_CFLAGS) -c $< -o $@

$(BUILD_DIR)/pocket_runtime.o: ../iphone2g/pocket_runtime.c | $(BUILD_DIR)
$(GCC) $(COMMON_CFLAGS) -c $< -o $@

$(BUILD_DIR)/app_data.o: check-app-data $(APP_DATA) | $(BUILD_DIR)
$(GCC) $(COMMON_CFLAGS) -c $(APP_DATA) -o $@

$(BUILD_DIR)/qjs/%.o: $(QJS_SRC)/%.c | $(BUILD_DIR)/qjs
$(GCC) $(QJS_CFLAGS) -c $< -o $@

$(BUILD_DIR)/pocketjs-cx2.elf: core $(HOST_OBJS) $(QJS_OBJS) | $(BUILD_DIR)
$(LD) $(HOST_OBJS) $(QJS_OBJS) $(CORE_ARCHIVE) -o $@ $(LDFLAGS) -lm

$(DIST_DIR)/pocketjs-cx2.tns: $(BUILD_DIR)/pocketjs-cx2.elf | $(DIST_DIR)
$(GENZEHN) --input $< --output $(BUILD_DIR)/pocketjs-cx2.zehn \
--name "PocketJS CX II" --uses-lcd-blit true
$(MAKE_PRG) $(BUILD_DIR)/pocketjs-cx2.zehn $@

clean:
rm -rf $(BUILD_DIR)
rm -f $(DIST_DIR)/pocketjs-cx2.tns
102 changes: 102 additions & 0 deletions hosts/nspire/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# TI-Nspire CX II / Ndless host

This development host runs one PocketJS guest on a **TI-Nspire CX II only**.
It embeds the compiled JavaScript and pak into one Ndless `.tns` program,
executes the guest with QuickJS, rasterizes the ordinary PocketJS DrawList in
software, converts its top-left BGRA output to RGB565, and presents it through
Ndless `lcd_blit`.

The development target is `nspire-cx2-dev`, with a fixed 320x240 logical and
physical viewport at raster density 1. It advertises only:

- `input.buttons`
- `input.analog.left` (the CX II touchpad sampled as two axes)
- `input.cursor` (the framework's synthesized cursor)
- `text.glyphs.baked`

There is deliberately no audio, touchscreen, network, filesystem, or runtime
font capability. A manifest requiring one of those is rejected before its
guest bundle is produced. The touchpad is controller input, not
`input.touch`.

## Toolchain

Install the current Ndless SDK and put `nspire-gcc`, `nspire-ld`, `genzehn`,
and `make-prg` on `PATH`. The host targets the SDK's ARM926EJ-S ABI:
ARMv5TE, ARM mode, EABI, soft float. Rust uses the pinned nightly already used
by PocketJS's standalone `no_std` core and builds `core`, `alloc`, and
`compiler_builtins` for `targets/armv5te-nspire-eabi.json`.

Check the local setup and fetch PocketJS's pinned QuickJS revision:

```sh
bun install --frozen-lockfile
bun nspire doctor
bun nspire bootstrap
```

`bootstrap` writes its checkout under ignored `dist/nspire/` and applies the
small `tools/nspire/quickjs-cx2.patch` portability patch (Newlib's `int32_t`
typedef differs from the PSP SDK's). It does not use an arbitrary system QuickJS. To use an existing checkout instead, set
`POCKETJS_QUICKJS_DIR` to its `libquickjs-sys/embed/quickjs` directory.

## Build

The included demo reuses the Hero application at the CX II viewport:

```sh
bun nspire bundle
bun nspire build
# dist/nspire/pocketjs-cx2.tns
```

Build another app with a CX II-compatible manifest:

```sh
bun nspire build --manifest=path/to/pocket.json
```

### Input acceptance guest

The input guest shows the **currently held CX II matrix keys**, the PocketJS
button mask, the **raw 0-255 analog axes**, and a touchpad-position marker:

```sh
bun nspire build --manifest=hosts/nspire/input-test/pocket.json
# dist/nspire/pocketjs-cx2.tns
```

The left panel updates from a host-only diagnostic string and does not add
calculator keys to the PocketJS button ABI. It recognizes the numeric,
alphabetic, operator, navigation, modifier, document, menu, and touchpad-click
matrix entries exposed by Ndless. Moving across the touchpad moves the amber
marker across the outlined range; lifting the finger reports `X 128 Y 128`
because the host centers an absent analog sample.

Copy `pocketjs-cx2.tns` to the calculator and open it with Ndless. Hold
**Ctrl+Esc** to leave the runtime. Default controls are:

| CX II key | Pocket button |
| --- | --- |
| arrows | D-pad |
| Ctrl / Enter / touchpad click | confirm (Circle) |
| Esc | back (Cross) |
| Shift | Square |
| Tab | Triangle |
| Menu | Start |
| Var | Select |
| touchpad position | analog axis / synthesized cursor |

## Acceptance boundary

The host remains outside PocketJS's production `POCKET_TARGETS` registry until
a CX II run proves all of the following:

- Ndless loads the generated `.tns` and QuickJS evaluates the embedded guest.
- The first Hero frame has correct RGB565 channel order and orientation.
- D-pad focus, confirm/back edges, touchpad cursor, and Ctrl+Esc work.
- A sustained animation does not exhaust memory and input remains responsive.
- Exiting restores the OS framebuffer mode.

Host-side tests cover the target contract and BGRA-to-RGB565 conversion, but
they are not substitutes for these calculator checks.
36 changes: 36 additions & 0 deletions hosts/nspire/compat.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/* Bare-metal compatibility for the single-threaded Ndless process. */

#include <stdint.h>

uint8_t __atomic_load_1(const volatile void *pointer, int order) {
(void)order;
return *(const volatile uint8_t *)pointer;
}

uint16_t __atomic_load_2(const volatile void *pointer, int order) {
(void)order;
return *(const volatile uint16_t *)pointer;
}

unsigned int __atomic_load_4(const volatile void *pointer, int order) {
(void)order;
return *(const volatile unsigned int *)pointer;
}

void __atomic_store_1(volatile void *pointer, uint8_t value, int order) {
(void)order;
*(volatile uint8_t *)pointer = value;
}

void __atomic_store_2(volatile void *pointer, uint16_t value, int order) {
(void)order;
*(volatile uint16_t *)pointer = value;
}

void __atomic_store_4(volatile void *pointer, unsigned int value, int order) {
(void)order;
*(volatile unsigned int *)pointer = value;
}

/* Arch's generic ARM Newlib references this hook; Ndless owns teardown. */
void _fini(void) {}
Loading