Skip to content

Commit d137366

Browse files
committed
tooling: headless real-font screenshot via the Epsilon Linux simulator
Add docker/screenshot.sh + docker/Dockerfile.capture + docker/capture.sh and a 'make screenshot' target: build the Epsilon Linux simulator, load the app as a native shared object with a framebuffer-dumping entry point (experimental/screenshot_main.cpp), and write capture/render.png with the real NumWorks font. Documents why the web simulator can't load the app yet.
1 parent 2f0c6a0 commit d137366

7 files changed

Lines changed: 180 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ tests/test_engine
55
*.nwa
66
node_modules/
77
.DS_Store
8+
capture/

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ install: $(OUTPUT_DIR)/$(APP_NAME).nwa
7272
test:
7373
$(MAKE) -C tests run
7474

75+
# Render the app in the Epsilon Linux simulator -> capture/render.png (Docker).
76+
.PHONY: screenshot
77+
screenshot:
78+
docker/screenshot.sh
79+
7580
.PHONY: clean
7681
clean:
7782
rm -rf $(OUTPUT_DIR)

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,20 @@ Then open <http://localhost:8000/epsilon.html?nwb=/rpn.nwb>. The first run build
129129
the simulator (long; cached in a Docker volume afterwards). You can also build
130130
the web app alone with `make PLATFORM=web` (needs `emcc`).
131131

132+
> **Note.** Loading the app in the *web* simulator is not wired up yet (our side
133+
> module needs libc/soft-float symbols Epsilon's size-deduped main module does
134+
> not export). To see a real-font render, use the native capture below.
135+
136+
### Real-font screenshot (Docker)
137+
138+
```shell
139+
make screenshot # or: docker/screenshot.sh
140+
```
141+
142+
Builds the Epsilon **Linux** simulator, renders the app headless with a sample
143+
stack, and writes `capture/render.png` — the actual NumWorks font, no browser
144+
needed.
145+
132146
## Project layout
133147

134148
```

docker/Dockerfile.capture

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Native (non-emscripten) environment to render the app in the Epsilon Linux
2+
# simulator and capture a real-font screenshot. Pass --platform matching your
3+
# host (docker/screenshot.sh does this) so host tools are built for the native
4+
# architecture — running an amd64 build tool under Rosetta breaks the font step.
5+
FROM ubuntu:24.04
6+
7+
RUN apt-get update && apt-get install -y --no-install-recommends \
8+
build-essential git python3 python3-pil python3-lz4 imagemagick \
9+
pkg-config libfreetype-dev libsdl2-dev libpng-dev libjpeg-dev \
10+
nodejs npm ca-certificates \
11+
&& rm -rf /var/lib/apt/lists/*
12+
13+
COPY capture.sh /usr/local/bin/capture.sh
14+
RUN chmod +x /usr/local/bin/capture.sh
15+
16+
WORKDIR /work
17+
ENTRYPOINT ["/usr/local/bin/capture.sh"]

docker/capture.sh

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env bash
2+
# Container entrypoint: build the Epsilon Linux simulator (once), build the RPN
3+
# app as a native shared object with a screenshot entry point that dumps the
4+
# framebuffer, run it headless, and convert the dump to /out/render.png.
5+
# Mounts: /epsilon (fork root), /app (this repo), /out (output dir).
6+
set -euo pipefail
7+
8+
EPSILON=/epsilon/epsilon
9+
APP=/app
10+
OUT=/out
11+
BIN="$EPSILON/output/release/linux/epsilon.bin"
12+
13+
[ -f "$EPSILON/Makefile" ] || { echo "!! mount the Epsilon fork ROOT at /epsilon" >&2; exit 1; }
14+
15+
if [ ! -f "$BIN" ]; then
16+
echo ">> Building the Epsilon Linux simulator — first run is long (reused after)…"
17+
( cd "$EPSILON" && make PLATFORM=linux -j"$(nproc)" epsilon.bin )
18+
fi
19+
20+
CAP="$EPSILON/external_apps/rpn_capture"
21+
rm -rf "$CAP"; mkdir -p "$CAP/src"
22+
cp "$APP"/src/*.cpp "$APP"/src/*.h "$APP"/src/icon.png "$CAP/src/"
23+
rm -f "$CAP/src/main.cpp"
24+
cp "$APP"/experimental/screenshot_main.cpp "$CAP/src/main.cpp"
25+
sed -i 's#\.\./src/##' "$CAP/src/main.cpp"
26+
printf 'APP_NAME = rpn_capture\nAPP_ICON = src/icon.png\nOUTPUT_DIR = output\nSOURCES = $(wildcard src/*.cpp)\ninclude ../build/external_app.mak\n' > "$CAP/Makefile"
27+
28+
echo ">> Building the app as a native shared object…"
29+
( cd "$CAP" && make PLATFORM=linux SIMULATOR="$BIN" build )
30+
NWB="$CAP/output/linux/rpn_capture.nwb"
31+
32+
echo ">> Rendering headless and capturing the framebuffer…"
33+
( cd "$EPSILON" && timeout 40 "$BIN" --headless --take-screenshot /tmp/ignore.png --nwb "$NWB" || true )
34+
python3 -c "from PIL import Image; Image.open('$OUT/shot.ppm').save('$OUT/render.png')"
35+
rm -rf "$CAP" "$OUT/shot.ppm"
36+
echo ">> Wrote /out/render.png"

docker/screenshot.sh

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/usr/bin/env bash
2+
# Render the app in the Epsilon Linux simulator and write a real-font screenshot
3+
# to capture/render.png — only Docker is needed.
4+
#
5+
# docker/screenshot.sh
6+
#
7+
# Needs an Epsilon fork ROOT next to this repo (../epsilon), i.e. the folder
8+
# containing epsilon/, haussmann/, poincare/, shared/. Override with
9+
# EPSILON_DIR=/path/to/epsilon docker/screenshot.sh
10+
set -euo pipefail
11+
12+
APP_DIR="$(cd "$(dirname "$0")/.." && pwd)"
13+
EPSILON_DIR="${EPSILON_DIR:-$APP_DIR/../epsilon}"
14+
OUT_DIR="${OUT_DIR:-$APP_DIR/capture}"
15+
16+
if [ ! -f "$EPSILON_DIR/epsilon/Makefile" ]; then
17+
echo "Epsilon fork root not found at: $EPSILON_DIR" >&2
18+
echo "Set EPSILON_DIR to the fork root (contains epsilon/, haussmann/, …)." >&2
19+
exit 1
20+
fi
21+
EPSILON_DIR="$(cd "$EPSILON_DIR" && pwd)"
22+
mkdir -p "$OUT_DIR"
23+
24+
# Build host tools for the native arch (an amd64 tool under Rosetta breaks fonts).
25+
case "$(uname -m)" in
26+
arm64|aarch64) PLATFORM=linux/arm64 ;;
27+
*) PLATFORM=linux/amd64 ;;
28+
esac
29+
30+
docker build --platform "$PLATFORM" -t numworks-rpn-capture \
31+
-f "$APP_DIR/docker/Dockerfile.capture" "$APP_DIR/docker"
32+
docker run --rm --platform "$PLATFORM" \
33+
-v "$EPSILON_DIR":/epsilon \
34+
-v "$APP_DIR":/app \
35+
-v "$OUT_DIR":/out \
36+
numworks-rpn-capture
37+
38+
echo "Wrote $OUT_DIR/render.png"

experimental/screenshot_main.cpp

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Screenshot build of the RPN app (native simulator only): prefills a stack of
2+
// representative symbolic values, draws once, then reads the framebuffer back
3+
// with eadk_display_pull_rect and writes it as a PPM to /out/shot.ppm. Used by
4+
// the headless capture harness; never shipped.
5+
extern "C" {
6+
#include <stdint.h>
7+
#include <stdio.h>
8+
}
9+
10+
#include "../src/eadkpp.h" // brings in eadk.h (pull_rect, eadk_rect_t, eadk_color_t)
11+
#include "../src/rpn.h"
12+
#include "../src/view.h"
13+
14+
const char eadk_app_name[]
15+
#if PLATFORM_DEVICE
16+
__attribute__((section(".rodata.eadk_app_name")))
17+
#endif
18+
= "RPN";
19+
const uint32_t eadk_api_level
20+
#if PLATFORM_DEVICE
21+
__attribute__((section(".rodata.eadk_api_level")))
22+
#endif
23+
= 0;
24+
25+
static void feed(Engine& e, const char* digits) {
26+
for (const char* p = digits; *p; p++) {
27+
char s[2] = {*p, 0};
28+
e.appendText(s);
29+
}
30+
}
31+
32+
int main(int argc, char* argv[]) {
33+
(void)argc;
34+
(void)argv;
35+
Engine e;
36+
feed(e, "1"); e.execute(Command::EnterOrDup);
37+
feed(e, "2"); e.execute(Command::Div); // 1/2
38+
feed(e, "2"); e.execute(Command::Sqrt);
39+
feed(e, "3"); e.execute(Command::Sqrt);
40+
e.execute(Command::Add); // √2 + √3
41+
feed(e, "1"); e.execute(Command::EnterOrDup);
42+
feed(e, "2"); e.execute(Command::Sqrt); e.execute(Command::Add);
43+
feed(e, "2"); e.execute(Command::Pow); // (1+√2)^2 = 3 + 2√2
44+
e.execute(Command::PushPi);
45+
feed(e, "2"); e.execute(Command::Div); // π/2
46+
47+
View view;
48+
view.draw(e, false, 0);
49+
50+
const int w = 320, h = 240;
51+
static eadk_color_t buf[320 * 240];
52+
eadk_rect_t full = {0, 0, w, h};
53+
eadk_display_pull_rect(full, buf);
54+
55+
FILE* f = fopen("/out/shot.ppm", "wb");
56+
if (f) {
57+
fprintf(f, "P6\n%d %d\n255\n", w, h);
58+
for (int i = 0; i < w * h; i++) {
59+
uint16_t c = buf[i]; // RGB565
60+
unsigned char rgb[3] = {
61+
static_cast<unsigned char>(((c >> 11) & 0x1F) << 3),
62+
static_cast<unsigned char>(((c >> 5) & 0x3F) << 2),
63+
static_cast<unsigned char>((c & 0x1F) << 3)};
64+
fwrite(rgb, 1, 3, f);
65+
}
66+
fclose(f);
67+
}
68+
return 0;
69+
}

0 commit comments

Comments
 (0)