New egui feature#31
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
new egui feature, with instructions & placeholder icon.