Skip to content

New egui feature#31

Merged
techomancer merged 15 commits into
techomancer:mainfrom
danifunker:new-egui-feature
Jun 2, 2026
Merged

New egui feature#31
techomancer merged 15 commits into
techomancer:mainfrom
danifunker:new-egui-feature

Conversation

@danifunker
Copy link
Copy Markdown
Contributor

new egui feature, with instructions & placeholder icon.

danifunker and others added 15 commits May 31, 2026 23:19
New workspace crate iris-gui — an opt-in launcher and runtime host for
iris built on eframe/egui. Default `cargo build` is unchanged; opt in
with `cargo build -p iris-gui --release`.

What it does:

  * Menu-driven config (snow-style). File menu manages named machines
    stored in ~/.config/iris/gui.json with debounced autosave; iris.toml
    is import/export only. SCSI menu attaches/detaches/swaps per ID,
    with a "drive present, no media" option. Memory menu picks RAM
    totals or per-bank. View menu has fullscreen + UI zoom.
  * Embedded REX3 framebuffer via a custom Renderer impl installed in
    Rex3::renderer; pixels uploaded to an egui texture each frame and
    drawn aspect-fit in the central panel.
  * PS/2 input routed from egui — modifier diff-synthesis, full Key →
    winit::KeyCode map, mouse only active over the framebuffer rect.
  * Save state / Restore state / Screenshot wired to
    Machine::save_snapshot / ci_restore / a PNG dump of the framebuffer.
  * Panic-safe: worker wraps Machine::{new,start,stop} in catch_unwind;
    a missing-SCSI-image preflight modal sidesteps the iris fatal-exit
    path; new IRIS_NO_EXIT_ON_POWEROFF env var skips the soft-power-off
    process::exit when iris is embedded.

iris core changes:

  * build_features module exposes CHD / CAMERA / JIT / REX_JIT /
    LIGHTNING as pub const bool so iris-gui can adapt the UI.
  * ScsiDevice.backend is now Option<DiskBackend>: lets a CD-ROM drive
    exist with no media inserted. SCSI command dispatch returns
    NOT READY + sense 0x3A (MEDIUM NOT PRESENT) on TEST UNIT READY /
    READ CAPACITY / READ / READ TOC when empty. New
    Wd33c93a::insert_disc / eject_to_empty helpers and a
    ScsiDevice::new_empty_cdrom constructor.
  * MachineConfig::validate no longer rejects an empty-path CD-ROM.
  * machine.rs PowerOff and ci.rs `quit` honour IRIS_NO_EXIT_ON_POWEROFF
    so embedders don't get killed by guest events.

See iris-gui-README.md for the full tour and rules/gui/01-overview.md
for the architecture notes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- input: route framebuffer mouse/keyboard through click-to-capture
  (grab + hide cursor, raw MouseMoved deltas, Esc/focus-loss to release).
  Fixes guest-pointer drift/misalignment and the keyboard leak into the
  config side panel. Auto-releases when the emulator stops.
- main: move the config editor into a collapsible right side panel so the
  REX3 framebuffer is always visible; add an on-screen capture hint.
- view: UI scale defaults to 1.0; slider applies via an explicit Apply
  button (live re-zoom fought the slider). Ctrl+0 reset now 1.0.
- z85c30: serial TCP backend falls back to a null backend with a warning
  instead of .expect()-aborting when the port is already in use.
- handle: bump the iris-gui-emu worker stack to 64 MB so Machine::new ->
  Rex3::new doesn't overflow in debug builds.
- docs: README UI-tour + serial-port notes; rules/gui mouse-integration
  analysis (capture vs snow's absolute low-memory pattern).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- config/machine: add VinoSource::Off (VINO stays mapped but no source or
  DMA thread) and make it the default — skips Video-In, the camera prompt,
  and a worker thread unless explicitly enabled.
- safe_stop: decide from config instead of never-populated runtime signals.
  Power off without a dialog unless a read-write base-image disk is attached
  (CD-ROM / COW overlay / scratch / CHD all leave the base image untouched).
- handle: bound Machine::stop() at 5s via a detached helper thread
  (stop_machine_timed) so a wedged guest can't freeze Stop or hang Quit.
- input: release mouse capture with Ctrl+Alt+Esc (Option on macOS) instead
  of bare Esc, so plain Esc reaches the guest.
- docs: README Video-In options + safe-stop/wedged-machine sections; rules
  note updated for the release chord.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- config: fix iris.toml export, which failed with "map key was not a
  string". Serialize the u8-keyed scsi map through string keys (toml needs
  string keys) and reorder MachineConfig so table fields come last (toml
  requires scalars before tables); skip empty optionals. Adds a round-trip
  test.
- scsi_menu: attaching a CD-ROM now creates an empty drive by default
  ("Attach CD-ROM drive (empty)"); load media via Insert disc. Drop the
  now-unused AttachCdrom action.
- config_ui (Disks tab): switching a device Type to CD-ROM clears the
  auto-generated scsiN.raw placeholder so it defaults to empty, with a hint.
- config_ui/main: replace the close-button glyph ✕ (U+2715, not in egui's
  bundled fonts — rendered as a tofu box) with × (U+00D7).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- monitor: fail soft (warn + disable) instead of .expect()-aborting when the
  TCP port can't be bound — e.g. a prior machine's monitor thread from the
  same session still holds 8888. Matches the z85c30 serial fix.
- handle: add EmulatorHandle::shutdown() (stop machine + join worker, bounded
  by the stop timeout); on_exit now calls it so a running guest is cleaned up
  synchronously even when the user closes the window without pressing Stop.
  Drop delegates to it as a backstop.
- single_instance: on startup, terminate a still-alive previous iris-gui
  (crash/hang or forgotten copy that would keep monitor/serial ports bound):
  SIGTERM then SIGKILL, verified via ps to be an iris-gui. Records our PID;
  removes it on clean exit. Unix only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirror rusty-backup's icon pipeline for the optional iris-gui front-end:
- scripts/generate-icon.sh, add-transparency.sh (adapted to iris-gui paths)
- iris-gui/assets/icon-original.png source + generated icons/ (PNG sizes,
  .ico, .icns, AppImage hicolor tree)
- main.rs decodes icon-256.png via the existing png crate and sets the
  viewport icon and app_id ("iris-gui")

No build-script changes: generation stays manual since iris-gui is optional.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full-bleed artwork rendered edge-to-edge and looked oversized next to other
Dock icons. Scale artwork to 80% of each canvas (≈10% transparent margin per
side) to match Apple's icon grid; regenerate all sizes, .ico and .icns.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… coffdump, unrelated).

  During the rebase, the "default CD-ROM to empty" change converted ScsiDisk::backend from DiskBackend to Option<DiskBackend> — so an
  empty drive is None rather than a sentinel. The rest of the file was updated to wrap assignments in Some(...) (e.g. scsi.rs:206, scsi.rs:284), but load_disc
  at line 326 still did a bare assignment:

  self.backend = DiskBackend::Direct(f);   // before
  self.backend = Some(DiskBackend::Direct(f));   // after

  One-line fix, and it matches the convention used everywhere else in the file.
Two unrelated front-end bugs that both made the GUI look broken at runtime.

MIPS indicator never updated
----------------------------
The emulator worker blocked on `cmd_rx.recv()` and never emitted
`Evt::Status`, so the status-bar MIPS label was stuck at its 0.0 default
for the whole session. Drive a periodic status tick instead:

- worker_loop now uses `recv_timeout(500ms)`; on each idle tick while a
  machine is running it reads REX3's free-running cycle counter
  (`rex3.cycles`, the same atomic the CLI status bar samples), divides the
  delta by wall-clock, and emits `Evt::Status { mips, .. }`. The counter
  Arc is latched on Start and dropped on Stop/Quit.
- `drain_events` now *merges* the perf-derived fields (mips, dirty_cow)
  from `Evt::Status` rather than replacing the whole `Status`. The old
  `self.status = s.clone()` would have clobbered the event-driven
  `running` / `power_off_seen` / `in_prom` flags.

Black emulator screen (most visible on X11 / large displays)
------------------------------------------------------------
REX3 packs dither / overlay bits into the high byte of each framebuffer
pixel (e.g. the Bayer index in `bayer_pack`), not a 0xFF alpha. iris's own
glow renderer ignores this because its main pass draws with blending
disabled, but egui always composites textures with alpha blending, so
`ColorImage::from_rgba_unmultiplied` interpreted that near-zero high byte
as transparency and rendered the framebuffer (near-)transparent over the
black central panel. egui chrome (opaque panel fills) rendered fine, so
only the emulator area went black -- matching the field reports.

Force the alpha byte to 0xFF in the CaptureRenderer copy so the captured
frame is opaque. The high byte is meaningless for display, so this is
safe and backend-independent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The standalone iris renderer does a single direct glTexSubImage from REX3's
strided framebuffer; the GUI path was doing several extra full-buffer passes
per frame, costing ~10% emulator throughput. Two of those are pure waste:

Lock-free frame gating
----------------------
`framebuffer_panel` called `FrameSink::snapshot()` — a mutex lock plus a full
(~5 MB at 1280x1024) clone of the frame — on *every* 60 fps repaint, even when
REX3 had not produced a new frame. That is ~300 MB/s of pointless copying and
needless lock contention against the REX3 refresh thread (which blocks on the
same mutex when it writes).

FrameSink now mirrors its sequence number into an AtomicU64. The GUI does a
lock-free `seq()` check each repaint and only locks + clones + re-uploads the
texture when the sequence actually advanced. The draw size is taken from the
texture handle instead of the just-cloned frame.

Fused capture copy
------------------
CaptureRenderer was doing `copy_from_slice` followed by a second full pass to
force the alpha byte to 0xFF (REX3 packs dither/overlay bits in the high byte,
not opacity, and egui composites with alpha blending). Merged into a single
pass per pixel: `word | 0xFF00_0000` writes R,G,B verbatim and sets opaque
alpha in one write, halving capture-thread memory traffic per dirty frame.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The compiler thread printed "REX JIT: compiled dm0=... (NB, total: N)" via an
unconditional eprintln! on every successful shader compile. That fires once
per unique (DrawMode0, DrawMode1) pair — dozens to hundreds on first boot —
spamming the console and looking like an error to users, when it is purely
informational and a sign the rex-jit path is working.

Route it through the existing gated dev log (dlog!(LogModule::Rex3, ...))
so it is silent by default and opt-in via IRIS_DEBUG_LOG=rex3 or the monitor
`log rex3` command. Genuine error paths (compile/finalize failures) and the
one-time startup lines are left as plain eprintln!.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The View-menu slider used range 0.75..=2.5 while the Ctrl +/-/0 keyboard zoom
used 0.5..=3.0. A scale set below 0.75 via the keyboard (or any stale value)
was persisted and applied, but the slider could not represent it, so egui
silently clamped and rewrote it to its own 0.75 minimum the moment the View
menu opened — surfacing as a confusing "0.750" default.

- Introduce shared UI_SCALE_MIN (0.5) / UI_SCALE_MAX (3.0) / UI_SCALE_DEFAULT
  (1.25) and use them for the slider, the keyboard zoom, and the startup zoom,
  so all three agree on the range.
- Raise the fresh-install default scale from 1.0 to 1.25 (testers on large
  displays found the UI too small).
- Clamp ui_scale on load into the supported range (and fall back to the
  default on a non-finite/corrupt value) so a stale persisted value can no
  longer put the UI into a state the slider has to silently re-clamp.
- Ctrl+0 now resets to UI_SCALE_DEFAULT rather than a hardcoded 1.0.

Existing saved scales within range are left untouched by design; Ctrl+0 (which
persists on exit) or deleting gui.json moves an old config to the new default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s button

Two independent GUI fixes.

UI scale: heal stale sub-minimum values instead of exposing them
----------------------------------------------------------------
The prior commit (0ff9b82) widened the scale floor to 0.5 and merely clamped
on load. That backfired: configs written by the old build (whose keyboard zoom
floored at 0.5) held sub-0.75 values that the old slider had only *displayed*
as 0.750 while the UI actually rendered that small. Widening the slider just
surfaced the real, tiny value (e.g. 0.500) rather than fixing it.

- UI_SCALE_MIN is back to 0.75 — a consistent floor for the slider and the
  Ctrl +/- keyboard zoom. Sub-0.75 is not useful for the launcher and was the
  source of the "UI too small / can't see it" reports.
- On load, a persisted scale below the minimum (or a non-finite value from a
  corrupt file) is reset to UI_SCALE_DEFAULT (1.25) rather than honored or
  clamped to the floor. Because the UI can no longer produce sub-minimum
  values, this only touches leftover configs from the old build and cannot
  override a deliberate future choice. The high end is still clamped to 3.0.

Edit Disks button in the missing-disk modal did nothing
-------------------------------------------------------
"Edit Disks tab" set the active tab to Disks but left the config-editor side
panel collapsed (its default state), so the tab switch was invisible and the
button looked dead. It now also sets show_config_editor = true so the editor
opens on the Disks tab.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clicking the framebuffer to capture the mouse worked on macOS but broke on
Linux/X11: the cursor was hidden but never actually grabbed, so it drifted to
the screen edge (motion stalled) and the moment it left the window the
focus-loss guard silently dropped the capture.

Root cause traced through the deps:
- input.rs requested CursorGrab::Locked unconditionally.
- winit 0.30's X11 backend rejects Locked with NotSupported (it only supports
  Confined); Wayland/macOS are the opposite (Locked only, no Confined).
- egui-winit does no fallback — it just logs the error — so the grab silently
  never happened on X11.

Pick the mode winit actually supports per platform/session: Confined on X11
(keeps the cursor in-window; raw DeviceEvent::MouseMotion deltas still arrive,
so relative motion to the guest is unaffected), Locked on Wayland/macOS/Windows.
Wayland is detected via WAYLAND_DISPLAY.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Configs holding exactly 0.75 were kept as-is by the previous heal logic (the
reset only fired below the minimum), so the UI still came up too small. Raise
UI_SCALE_MIN to 1.0: the slider/keyboard floor is now 1.0, and any persisted
value below it (including the lingering 0.75) is reset to the 1.25 default on
load.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@techomancer techomancer merged commit 73e2d00 into techomancer:main Jun 2, 2026
1 check passed
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