Skip to content

Add a cross-platform SDL emulator for developing the goggle UI without hardware#617

Draft
christhomas wants to merge 1 commit into
hd-zero:mainfrom
christhomas:feat/sdl-emulator
Draft

Add a cross-platform SDL emulator for developing the goggle UI without hardware#617
christhomas wants to merge 1 commit into
hd-zero:mainfrom
christhomas:feat/sdl-emulator

Conversation

@christhomas

Copy link
Copy Markdown

Adds a desktop build of the goggle UI (Linux, macOS, Windows) that runs the real
application code — the same main(), menu system, OSD and page logic that ships on the
goggle — rendered through SDL2 instead of the framebuffer, with the hardware peripherals
replaced by compiled-in mocks.

The aim is to make the LVGL interface developable and previewable without hardware, and
to give CI a way to catch UI/driver regressions on every push.

Build and run

.devcontainer/build.sh images     # build the devcontainer images (one-time)
.devcontainer/build.sh linux      # -> build_emu_linux/HDZGOGGLE
.devcontainer/build.sh windows    # -> build_win_x64/HDZGOGGLE.exe   (mingw cross)
.devcontainer/build.sh mac        # -> build_xmac/HDZGOGGLE
./run-native.sh                   # launch it in a window

Every target compiles inside a devcontainer image, so builds are reproducible and need
no host toolchain; the resulting binary runs natively on its OS. macOS is the one exception
and builds on a mac, since a cross image would need Apple's non-redistributable SDK.

Zero cost to the firmware

All emulator code sits behind EMULATOR_BUILD or in src/emulator/*, which CMake compiles
only for the emulator — the goggle firmware binary contains none of it. Hardware drivers
that genuinely differ (UART, I2C, RTC, the framebuffer) are split into per-target files
selected by CMake, so the goggle drivers carry no emulator #ifdefs.

Verified by building the firmware from this branch: it compiles unchanged, with zero
emulator objects linked in.

What's mocked (at the hardware seam, so the app itself is untouched)

Hardware Emulator stand-in
Display engine / framebuffer SDL2 window + software compositor
DM5680 video receiver decodes a looping MPEG-1 clip (vendored pl_mpeg; no ffmpeg is linked) behind the OSD, and only in the video view
DM5680 serial link socketpair-backed UART + a mock answering version/valid/RSSI
I2C bus, RTC no-op bus, in-memory clock
Device shell commands (system_exec) logged and skipped — never touches the host

Also included: configurable filesystem roots (path_app()/path_extsd() from a config file

  • env, compiled away on the goggle so the on-device /mnt/app and /mnt/extsd defaults are
    unchanged), and POSIX compat shims for macOS/Windows hosts.

CI

.github/workflows/build_emulator.yml builds all three targets on every push/PR with a
headless screenshot as a regression check, and publishes per-platform binaries on a semver
tag. The existing firmware workflows are untouched (and pass on this branch).

Notes

Adds a desktop build of the goggle UI (Linux/macOS/Windows) that runs the real
application code -- the same main(), menu system, OSD and page logic that ships
on the goggle -- rendered through SDL2 instead of the framebuffer, with the
hardware peripherals replaced by compiled-in mocks. This lets the LVGL interface
be developed, previewed and regression-tested without hardware.

Build and run:
    .devcontainer/build.sh {images,linux,windows,mac}   build (in a container)
    ./run-native.sh                                     run natively on the host

Every target compiles inside a devcontainer image, so builds are reproducible and
need no host toolchain; the resulting binary runs natively on its OS. macOS is
cross-compiled to Mach-O in the container too (LLVM/clang); its SDK is staged once
from your Xcode since Apple's is not redistributable. CI is the one place macOS
builds natively -- on a macos-latest runner -- because CI can't stage that SDK.

Zero cost to the firmware: all emulator code lives behind EMULATOR_BUILD or in
src/emulator/*, which CMake compiles only for the emulator, so the goggle binary
contains none of it. Hardware drivers that genuinely differ (UART, I2C, RTC, the
framebuffer) are split into per-target files selected by CMake, keeping the goggle
drivers free of emulator #ifdefs.

Peripherals are mocked at their hardware seam, so the app itself is unchanged:
  - display engine  -> SDL2 window + software compositor
  - DM5680 video    -> decodes a looping MPEG-1 clip (vendored pl_mpeg; no ffmpeg
                       is linked) behind the OSD, and only in the video view
  - DM5680 serial   -> socketpair-backed UART + a mock answering version/RSSI
  - I2C bus, RTC    -> no-op bus, in-memory clock
  - system_exec     -> logged and skipped, so device shell commands (display pokes,
                       register writes, wifi/passwd scripts) never touch the host

Also included: configurable filesystem roots (path_app/path_extsd from config+env,
compiled away on the goggle), POSIX compat shims for macOS/Windows hosts, a fix for
ATOMIC_VAR_INIT removed on newer toolchains (which the emulator build requires; also
offered standalone), and CI that builds all three targets on every push with a
headless screenshot, publishing binaries on a semver tag.
@nerdCopter

Copy link
Copy Markdown
Contributor

AI Generated comment

Problem: build.sh linux (and windows/mac) calls docker run against hdzero-dev:latest without checking whether the image has been built first. If it hasn't, Docker tries to pull hdzero-dev from a registry and fails with pull access denied, which reads as a login/registry issue rather than a missing local build step.

Proposed solution: Have linux/windows/mac check for the image (e.g. docker image inspect) and run images automatically if missing, or at minimum fail with a clear message pointing to build.sh images. Also worth a one-line note in the README or .devcontainer about running build.sh images before the first per-target build, since this CLI flow isn't covered by the existing VS Code devcontainer docs.

@nerdCopter

Copy link
Copy Markdown
Contributor

AI Generated comment

Not authoritative — flagging for consideration, deferring to the author's judgment on implementation.

Problem: .devcontainer/Dockerfile builds FROM mcr.microsoft.com/devcontainers/base:ubuntu unpinned, which currently resolves to Ubuntu 26.04 LTS (glibc 2.43). Because glibc symbol versioning is forward-only, a binary built against 2.43 cannot run on any host with an older glibc — e.g. Ubuntu 24.04 (2.39), Ubuntu 22.04 (2.35), or Debian 13 (2.41) all fail with version 'GLIBC_2.43' not found when running run-native.sh after build.sh linux. This breaks the design stated in build.sh's own header: "compiled INSIDE a devcontainer image; each RUNS natively on its OS."

Some floor has to be fixed regardless of implementation — whether via an older base image, a fully static build, or explicit glibc-ABI targeting in the toolchain, all three amount to picking a minimum glibc version the output supports. Tracking whatever LTS is newest leaves that floor undefined and drifting upward every two years.

Proposed solution: Pin the build environment to a deliberately conservative glibc floor. Simplest: change FROM to a specific older LTS (ubuntu:24.04 or ubuntu:22.04). If staying on the newest base image/toolchain is preferred, the same floor can be achieved via static linking or -target-style glibc-version pinning in the compiler/linker instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants