Skip to content
Closed
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
22 changes: 22 additions & 0 deletions apps/d211-demo/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Hero from "../hero/app.tsx";
import { reportAppAction } from "@pocketjs/framework/host";

/**
* First guest on the D211 fbdev host: software raster, QuickJS, on the full
* 800x480 panel. The large layout is the wide-surface variant Hero already
* uses on 480x720 logical targets.
*/
export default function D211Hero() {
return (
<Hero
actionLabel="TOUCH THE SCREEN"
deviceLabel="running on an ArtInChip D211."
headline="JS on D211."
largeLayout
onAction={(count) => reportAppAction("hero_press", count)}
presentationHz={60}
runtimeLabel="RUST + QUICKJS + SOFTWARE"
spinnerFrameStep={6}
/>
);
}
5 changes: 5 additions & 0 deletions apps/d211-demo/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// @title PocketJS: D211 Linux
import { mount } from "@pocketjs/framework/solid";
import D211Hero from "./app.tsx";

mount(() => <D211Hero />);
24 changes: 24 additions & 0 deletions apps/d211-demo/pocket.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"$schema": "https://pocketjs.dev/schema/pocket-2.json",
"pocket": 2,
"id": "dev.pocket-stack.d211-demo",
"name": "pocketjs-d211-demo",
"title": "PocketJS: D211 Linux",
"version": "0.1.0",
"engine": {
"capabilities": {
"requires": ["input.touch", "text.glyphs.baked"]
}
},
"app": {
"entry": "apps/d211-demo/main.tsx",
"output": "d211-demo-main",
"framework": "solid",
"viewport": {
"fixed": {
"logical": [800, 480],
"presentation": "native"
}
}
}
}
1 change: 1 addition & 0 deletions docs/STRUCTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pocketjs/
│ ├─ blackberry-classic/ input sampling shared by both BlackBerry Classic hosts
│ ├─ blackberry-classic-qnx/ BlackBerry 10 Core Native embedding
│ ├─ blackberry-classic-android/ BlackBerry 10 Android Runtime embedding
│ ├─ d211-linux/ ArtInChip D211DBV fbdev host (software raster, evdev touch)
│ ├─ desktop/ gpui window host — macos-app + linux-app (standalone lone-bin crate)
│ ├─ web/ browser dev + Pocket System host (wasm core, isolated iframe Realms)
│ └─ sim/ deterministic headless simulation host (docs/DETERMINISM.md)
Expand Down
286 changes: 285 additions & 1 deletion engine/quickjs-c/pocket_runtime.c
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ static char reported_action_name[POCKETJS_ACTION_NAME_CAPACITY];
static int32_t reported_action_value;
static unsigned long reported_action_sequence;
static int runtime_failed;
static const PocketAudioOps *audio_ops;
static const PocketBacklightOps *backlight_ops;
static const PocketIpcOps *ipc_ops;
#ifdef POCKET_SVC_WIRE
/* spec SVC_POLL_BUF (8192) + terminator: one svcPoll batch. */
static char svc_poll_buffer[8193];
Expand Down Expand Up @@ -468,6 +471,286 @@ static JSValue host_operation(
return JS_ThrowInternalError(ctx, "unknown PocketJS HostOp");
}

/* ------------------------------------------------------------------ */
/* Host modules: audio (contracts/spec/audio.ts) and backlight. */
/* The host fills the op tables before boot; absent tables leave the */
/* globals unset and the framework player degrades to a no-op. */

void pocket_runtime_set_audio_ops(const PocketAudioOps *ops) {
audio_ops = ops;
}

void pocket_runtime_set_ipc_ops(const PocketIpcOps *ops) {
ipc_ops = ops;
}

void pocket_runtime_set_backlight_ops(const PocketBacklightOps *ops) {
backlight_ops = ops;
}

static JSValue host_audio_create_stream(
JSContext *ctx,
JSValueConst this_value,
int argc,
JSValueConst *argv
) {
uint32_t rate = 0;
uint32_t channels = 0;
if (!uint_argument(ctx, argc, argv, 0, &rate)) return JS_EXCEPTION;
if (!uint_argument(ctx, argc, argv, 1, &channels)) return JS_EXCEPTION;
return JS_NewInt32(ctx, audio_ops->create_stream(rate, channels));
}

static JSValue host_audio_destroy_stream(
JSContext *ctx,
JSValueConst this_value,
int argc,
JSValueConst *argv
) {
int32_t handle = 0;
if (!int_argument(ctx, argc, argv, 0, &handle)) return JS_EXCEPTION;
audio_ops->destroy_stream(handle);
return JS_UNDEFINED;
}

static JSValue host_audio_write_pcm(
JSContext *ctx,
JSValueConst this_value,
int argc,
JSValueConst *argv
) {
int32_t handle = 0;
const uint8_t *bytes = 0;
size_t length = 0;
if (!int_argument(ctx, argc, argv, 0, &handle)) return JS_EXCEPTION;
if (!bytes_argument(ctx, argc, argv, 1, &bytes, &length)) return JS_EXCEPTION;
return JS_NewInt32(ctx, audio_ops->write_pcm(handle, bytes, length));
}

static JSValue host_audio_play(
JSContext *ctx,
JSValueConst this_value,
int argc,
JSValueConst *argv
) {
int32_t handle = 0;
if (!int_argument(ctx, argc, argv, 0, &handle)) return JS_EXCEPTION;
audio_ops->play(handle);
return JS_UNDEFINED;
}

static JSValue host_audio_pause(
JSContext *ctx,
JSValueConst this_value,
int argc,
JSValueConst *argv
) {
int32_t handle = 0;
if (!int_argument(ctx, argc, argv, 0, &handle)) return JS_EXCEPTION;
audio_ops->pause(handle);
return JS_UNDEFINED;
}

static JSValue host_audio_stop(
JSContext *ctx,
JSValueConst this_value,
int argc,
JSValueConst *argv
) {
int32_t handle = 0;
if (!int_argument(ctx, argc, argv, 0, &handle)) return JS_EXCEPTION;
audio_ops->stop(handle);
return JS_UNDEFINED;
}

static JSValue host_audio_set_volume(
JSContext *ctx,
JSValueConst this_value,
int argc,
JSValueConst *argv
) {
int32_t handle = 0;
double volume = 1.0;
if (!int_argument(ctx, argc, argv, 0, &handle)) return JS_EXCEPTION;
if (argc < 2 || JS_ToFloat64(ctx, &volume, argv[1]) != 0) {
JS_ThrowTypeError(ctx, "missing argument 1");
return JS_EXCEPTION;
}
if (volume < 0.0) volume = 0.0;
if (volume > 1.0) volume = 1.0;
audio_ops->set_volume(handle, volume);
return JS_UNDEFINED;
}

static JSValue host_audio_end_stream(
JSContext *ctx,
JSValueConst this_value,
int argc,
JSValueConst *argv
) {
int32_t handle = 0;
if (!int_argument(ctx, argc, argv, 0, &handle)) return JS_EXCEPTION;
audio_ops->end_stream(handle);
return JS_UNDEFINED;
}

static JSValue host_audio_poll(
JSContext *ctx,
JSValueConst this_value,
int argc,
JSValueConst *argv
) {
const char *line = audio_ops->poll();
return line == 0 ? JS_UNDEFINED : JS_NewString(ctx, line);
}

static JSValue host_backlight_get(
JSContext *ctx,
JSValueConst this_value,
int argc,
JSValueConst *argv
) {
return JS_NewInt32(ctx, backlight_ops->get());
}

static JSValue host_backlight_set(
JSContext *ctx,
JSValueConst this_value,
int argc,
JSValueConst *argv
) {
int32_t percent = 100;
if (!int_argument(ctx, argc, argv, 0, &percent)) return JS_EXCEPTION;
if (percent < 0) percent = 0;
if (percent > 100) percent = 100;
backlight_ops->set(percent);
return JS_UNDEFINED;
}

/* One ipc.recv() datagram cap: MAX_PAYLOAD (64 KiB) + frame header. */
#define HOST_IPC_MAX_DATAGRAM 65552

static JSValue host_ipc_connect(
JSContext *ctx,
JSValueConst this_value,
int argc,
JSValueConst *argv
) {
if (ipc_ops == 0) return JS_NewBool(ctx, 0);
const char *path = 0;
size_t path_length = 0;
if (!string_argument(ctx, argc, argv, 0, &path, &path_length)) return JS_EXCEPTION;
int connected = ipc_ops->connect(path);
JS_FreeCString(ctx, path);
return JS_NewBool(ctx, connected >= 0);
}

static JSValue host_ipc_close(
JSContext *ctx,
JSValueConst this_value,
int argc,
JSValueConst *argv
) {
(void)ctx;
(void)this_value;
(void)argc;
(void)argv;
if (ipc_ops != 0) ipc_ops->close();
return JS_UNDEFINED;
}

static JSValue host_ipc_send(
JSContext *ctx,
JSValueConst this_value,
int argc,
JSValueConst *argv
) {
if (ipc_ops == 0) return JS_NewInt32(ctx, -1);
const uint8_t *bytes = 0;
size_t length = 0;
if (!bytes_argument(ctx, argc, argv, 0, &bytes, &length)) return JS_EXCEPTION;
if (length == 0) return JS_NewInt32(ctx, 0);
int written = ipc_ops->send(bytes, length);
return JS_NewInt32(ctx, written < 0 ? -1 : written);
}

static JSValue host_ipc_recv(
JSContext *ctx,
JSValueConst this_value,
int argc,
JSValueConst *argv
) {
uint8_t buffer[HOST_IPC_MAX_DATAGRAM];
if (ipc_ops == 0) return JS_NewArrayBufferCopy(ctx, buffer, 0);
int32_t capacity = HOST_IPC_MAX_DATAGRAM;
if (argc > 0 && !int_argument(ctx, argc, argv, 0, &capacity)) return JS_EXCEPTION;
if (capacity < 1) capacity = 1;
if (capacity > HOST_IPC_MAX_DATAGRAM) capacity = HOST_IPC_MAX_DATAGRAM;
int count = ipc_ops->recv(buffer, (size_t)capacity);
if (count < 0) count = 0;
return JS_NewArrayBufferCopy(ctx, buffer, (size_t)count);
}

static int add_module_function(
JSContext *ctx,
JSValueConst object,
const char *name,
int arity,
JSCFunction *function
) {
JSValue value = JS_NewCFunction(ctx, function, name, arity);
if (JS_IsException(value)) return 0;
return JS_SetPropertyStr(ctx, object, name, value) >= 0;
}

static int install_audio(void) {
JSValue audio;
if (audio_ops == 0) return 1;
audio = JS_NewObject(context);
if (JS_IsException(audio)) return 0;
if (!add_module_function(context, audio, "createStream", 2, host_audio_create_stream) ||
!add_module_function(context, audio, "destroyStream", 1, host_audio_destroy_stream) ||
!add_module_function(context, audio, "writePcm", 2, host_audio_write_pcm) ||
!add_module_function(context, audio, "play", 1, host_audio_play) ||
!add_module_function(context, audio, "pause", 1, host_audio_pause) ||
!add_module_function(context, audio, "stop", 1, host_audio_stop) ||
!add_module_function(context, audio, "setVolume", 2, host_audio_set_volume) ||
!add_module_function(context, audio, "endStream", 1, host_audio_end_stream) ||
!add_module_function(context, audio, "poll", 0, host_audio_poll)) {
JS_FreeValue(context, audio);
return 0;
}
return JS_SetPropertyStr(context, global, "audio", audio) >= 0;
}

static int install_backlight(void) {
JSValue backlight;
if (backlight_ops == 0) return 1;
backlight = JS_NewObject(context);
if (JS_IsException(backlight)) return 0;
if (!add_module_function(context, backlight, "get", 0, host_backlight_get) ||
!add_module_function(context, backlight, "set", 1, host_backlight_set)) {
JS_FreeValue(context, backlight);
return 0;
}
return JS_SetPropertyStr(context, global, "backlight", backlight) >= 0;
}

static int install_ipc(void) {
JSValue ipc;
if (ipc_ops == 0) return 1;
ipc = JS_NewObject(context);
if (JS_IsException(ipc)) return 0;
if (!add_module_function(context, ipc, "connect", 1, host_ipc_connect) ||
!add_module_function(context, ipc, "close", 0, host_ipc_close) ||
!add_module_function(context, ipc, "send", 1, host_ipc_send) ||
!add_module_function(context, ipc, "recv", 1, host_ipc_recv)) {
JS_FreeValue(context, ipc);
return 0;
}
return JS_SetPropertyStr(context, global, "ipc", ipc) >= 0;
}

static int add_host_operation(
JSContext *ctx,
JSValueConst object,
Expand Down Expand Up @@ -630,7 +913,8 @@ int pocket_runtime_boot(
}
REPORT_BOOT_STAGE(5);
global = JS_GetGlobalObject(context);
if (!install_host(width, height)) {
if (!install_host(width, height) || !install_audio() || !install_backlight() ||
!install_ipc()) {
take_exception(context);
pocket_runtime_shutdown();
return 0;
Expand Down
Loading