diff --git a/README.md b/README.md index 2570707f..e4e7959e 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ ZAR-Zig-Agent-Runtime is the Zig runtime port of OpenClaw, with parity-first del - Original OpenClaw baseline (`v2026.3.13-1`): `100/100` covered - Original OpenClaw beta baseline (`v2026.3.13-beta.1`): `100/100` covered - Union baseline: `141/141` covered (`MISSING_IN_ZIG=0`) -- Latest local validation: `zig build test --summary all` -> `398/398` passed +- Latest local validation: `zig build test --summary all` -> `399/399` passed - Current edge release target tag: `v0.2.0-zig-edge.31` - License posture: repo-wide `GPL-2.0-only` with Linux-style SPDX headers on repo-owned source and script files - Toolchain policy: Codeberg `master` is canonical; `adybag14-cyber/zig` publishes rolling `latest-master` and immutable `upstream-` Windows releases for refresh and reproducibility. @@ -44,8 +44,9 @@ ZAR-Zig-Agent-Runtime is the Zig runtime port of OpenClaw, with parity-first del - `src/pal/framebuffer.zig` exposes the framebuffer path through the bare-metal PAL and the bare-metal ABI now supports bounded mode switching plus supported-mode table export through `oc_framebuffer_set_mode`, `oc_framebuffer_supported_mode_count`, `oc_framebuffer_supported_mode_width`, and `oc_framebuffer_supported_mode_height` - `src/baremetal/edid.zig`, `src/baremetal/display_output.zig`, and `src/baremetal/virtio_gpu.zig` now provide EDID-backed display capability export plus bounded render/present/flush proof for the first real controller-specific path, `virtio-gpu-pci`, including digital-input, preferred-timing, CEA, DisplayID, HDMI-vendor-data, and basic-audio capability flags when present in EDID data - `src/baremetal/display_output.zig` now derives the exported connector type from EDID capability flags instead of hard-coding the virtio-gpu path as always `virtual` + - the same display-output surface now exports bounded per-output entries through `oc_display_output_entry_count` and `oc_display_output_entry`, and the broad host/tool surface now exposes `display-outputs` and `display-output ` on top of that table - `scripts/baremetal-qemu-framebuffer-console-probe-check.ps1` now proves live MMIO banner pixels plus exported adapter metadata against the freestanding PVH artifact at `640x400`, `1024x768`, and `1280x720` - - `scripts/baremetal-qemu-virtio-gpu-display-probe-check.ps1` now proves live `virtio-gpu-pci` EDID/controller capability export plus resource-create/attach/set-scanout/flush behavior with non-zero scanout pixel readback over QEMU with `edid=on` + - `scripts/baremetal-qemu-virtio-gpu-display-probe-check.ps1` now proves live `virtio-gpu-pci` EDID/controller capability export plus output-entry count/entry metadata and resource-create/attach/set-scanout/flush behavior with non-zero scanout pixel readback over QEMU with `edid=on` - real HDMI/DisplayPort connector-specific scanout paths are still future depth and are not claimed by this branch - keyboard/mouse is now strict-closed in [`docs/zig-port/FS5_5_HARDWARE_DRIVERS_SYSTEMS.md`](docs/zig-port/FS5_5_HARDWARE_DRIVERS_SYSTEMS.md) - `src/baremetal/ps2_input.zig` now contains a real x86 port-I/O backed PS/2 controller path diff --git a/docs/zig-port/FS5_5_HARDWARE_DRIVERS_SYSTEMS.md b/docs/zig-port/FS5_5_HARDWARE_DRIVERS_SYSTEMS.md index 6e2977b2..fc11e523 100644 --- a/docs/zig-port/FS5_5_HARDWARE_DRIVERS_SYSTEMS.md +++ b/docs/zig-port/FS5_5_HARDWARE_DRIVERS_SYSTEMS.md @@ -145,10 +145,10 @@ Current local source-of-truth evidence: - a real EDID-backed display capability path now exists beyond the rendered BGA console: - `src/baremetal/edid.zig` provides bounded EDID header/checksum/timing/name parsing - EDID parsing now also exports capability flags for digital input, preferred timing, CEA extension presence, DisplayID extension presence, HDMI vendor data, and basic audio when those descriptors are present - - `src/baremetal/display_output.zig` provides the exported display-output ABI surface plus EDID byte export - - `src/baremetal/virtio_gpu.zig` probes the first real controller-specific path, `virtio-gpu-pci`, through modern virtio PCI capabilities plus `GET_DISPLAY_INFO`, `GET_EDID`, bounded 2D resource creation, guest-backing attach, scanout selection, transfer-to-host, and flush + - `src/baremetal/display_output.zig` provides the exported display-output ABI surface plus EDID byte export and a bounded per-output entry table + - `src/baremetal/virtio_gpu.zig` probes the first real controller-specific path, `virtio-gpu-pci`, through modern virtio PCI capabilities plus `GET_DISPLAY_INFO`, `GET_EDID`, bounded multi-scanout enumeration, connector-aware scanout selection, bounded 2D resource creation, guest-backing attach, transfer-to-host, and flush - `src/pal/framebuffer.zig` now also exposes the display-output state and EDID byte surface through the PAL seam -- host regressions now prove the framebuffer export surface updates host-backed framebuffer state, glyph pixels, supported-mode enumeration, high-resolution mode switching, and preservation of the last valid mode on unsupported requests +- host regressions now prove the framebuffer export surface updates host-backed framebuffer state, glyph pixels, supported-mode enumeration, high-resolution mode switching, per-output entry export, and preservation of the last valid mode on unsupported requests - a live bare-metal PVH/QEMU proof now passes: - `scripts/baremetal-qemu-framebuffer-console-probe-check.ps1` - exported framebuffer state has `magic=framebuffer_magic`, `api_version=2`, and now proves `640x400` (`cols=80`, `rows=25`), `1024x768` (`cols=128`, `rows=48`), and `1280x720` (`cols=160`, `rows=45`) surfaces over the same BGA path @@ -159,7 +159,7 @@ Current local source-of-truth evidence: - a second live bare-metal PVH/QEMU proof now passes: - `scripts/baremetal-qemu-virtio-gpu-display-probe-check.ps1` - exported display-output state has `magic=display_output_magic`, `api_version=2`, `backend=virtio_gpu`, `controller=virtio_gpu`, an EDID-derived connector type, and a real EDID header over `virtio-gpu-pci,edid=on` - - runtime now also reports the selected virtio-gpu PCI vendor/device, PCI location, active scanout, current mode, preferred mode, physical dimensions, manufacturer/product IDs, exported EDID byte surface, and the exported capability flags derived from the EDID payload + - runtime now also reports the selected virtio-gpu PCI vendor/device, PCI location, active scanout, current mode, preferred mode, physical dimensions, manufacturer/product IDs, exported EDID byte surface, the exported capability flags derived from the EDID payload, and the bounded per-output entry export for the selected scanout - the same proof now also validates non-zero present statistics plus non-zero scanout pixels from the guest-backed render pattern after resource-create/attach/set-scanout/flush - current real source-of-truth rendered display support now covers bounded Bochs/QEMU BGA mode-setting plus virtio-gpu present/flush over the virtual scanout path - real HDMI/DisplayPort connector-specific scanout paths are not yet implemented and are not claimed by this branch diff --git a/docs/zig-port/PHASE_CHECKLIST.md b/docs/zig-port/PHASE_CHECKLIST.md index f1467f5d..a67116fa 100644 --- a/docs/zig-port/PHASE_CHECKLIST.md +++ b/docs/zig-port/PHASE_CHECKLIST.md @@ -9,12 +9,12 @@ Registry status: - `scripts/package-registry-status.ps1` now checks public npmjs/PyPI visibility correctly even when called with only `-ReleaseTag`, so local release diagnostics no longer silently skip the unresolved registry state. - release evidence now also includes `release-status.json` + `release-status.md`, which snapshot package visibility plus the latest `zig-ci` / `docs-pages` / `release-preview` / `npm-release` / `python-release` workflow state for the target tag. - `FS5.6` repo-wide license refresh is now strict-closed locally: root/package license files, release evidence, package metadata, and Linux-style SPDX headers now use `GPL-2.0-only` to match the Linux-derived RTL8139 slice. -- `FS5.5` framebuffer/console strict closure is now reached locally: `src/baremetal/framebuffer_console.zig` programs a real Bochs/QEMU BGA linear-framebuffer path with bounded mode support for `640x400`, `800x600`, `1024x768`, `1280x720`, and `1280x1024`, `src/baremetal/pci.zig` discovers the selected PCI display adapter as structured metadata, exposes the framebuffer BAR, and enables decode on that function, `src/baremetal/edid.zig`, `src/baremetal/display_output.zig`, and `src/baremetal/virtio_gpu.zig` now add the first real EDID-backed controller path over `virtio-gpu-pci` including exported capability flags for digital input, preferred timing, CEA, DisplayID, HDMI-vendor-data, and basic-audio metadata when present plus EDID-derived connector inference and bounded resource-create/attach/set-scanout/flush behavior, `src/pal/framebuffer.zig` exposes the surface plus supported-mode enumeration and display-output state through the PAL, host regressions in `src/baremetal/framebuffer_console.zig`, `src/baremetal/virtio_gpu.zig`, `src/baremetal_main.zig`, and `src/baremetal/display_output.zig` prove framebuffer state, display-output state, adapter metadata, supported-mode enumeration, glyph pixel updates, bounded mode switching, present counters, non-zero scanout pixels, and connector inference from EDID capability flags, and the live QEMU+GDB proofs `scripts/baremetal-qemu-framebuffer-console-probe-check.ps1` and `scripts/baremetal-qemu-virtio-gpu-display-probe-check.ps1` now read back real MMIO banner pixels plus BGA adapter metadata and real `virtio-gpu-pci` EDID/controller capability state with non-zero scanout pixels over the freestanding PVH artifact; real HDMI/DisplayPort connector-specific scanout paths remain future depth and are not claimed here. +- `FS5.5` framebuffer/console strict closure is now reached locally: `src/baremetal/framebuffer_console.zig` programs a real Bochs/QEMU BGA linear-framebuffer path with bounded mode support for `640x400`, `800x600`, `1024x768`, `1280x720`, and `1280x1024`, `src/baremetal/pci.zig` discovers the selected PCI display adapter as structured metadata, exposes the framebuffer BAR, and enables decode on that function, `src/baremetal/edid.zig`, `src/baremetal/display_output.zig`, and `src/baremetal/virtio_gpu.zig` now add the first real EDID-backed controller path over `virtio-gpu-pci` including exported capability flags for digital input, preferred timing, CEA, DisplayID, HDMI-vendor-data, and basic-audio metadata when present plus EDID-derived connector inference, bounded per-output entry export, and bounded resource-create/attach/set-scanout/flush behavior, `src/pal/framebuffer.zig` exposes the surface plus supported-mode enumeration and display-output state through the PAL, host regressions in `src/baremetal/framebuffer_console.zig`, `src/baremetal/virtio_gpu.zig`, `src/baremetal_main.zig`, and `src/baremetal/display_output.zig` prove framebuffer state, display-output state, adapter metadata, supported-mode enumeration, glyph pixel updates, bounded mode switching, present counters, non-zero scanout pixels, connector inference from EDID capability flags, and the output-entry table, and the live QEMU+GDB proofs `scripts/baremetal-qemu-framebuffer-console-probe-check.ps1` and `scripts/baremetal-qemu-virtio-gpu-display-probe-check.ps1` now read back real MMIO banner pixels plus BGA adapter metadata and real `virtio-gpu-pci` EDID/controller capability state with non-zero scanout pixels and validated output-entry metadata over the freestanding PVH artifact; real HDMI/DisplayPort connector-specific scanout paths remain future depth and are not claimed here. - `FS5.5` keyboard/mouse strict closure is now reached locally: `src/baremetal/ps2_input.zig` has a real x86 port-I/O backed PS/2 controller path (`0x60` / `0x64` status/data/command handling, config programming, output-buffer drain, mouse-byte packet assembly), the PAL input surface remains wired through `src/pal/input.zig`, host regressions in `src/baremetal_main.zig` assert IRQ-driven queue/payload semantics, and the live QEMU+GDB proof plus wrappers (`scripts/baremetal-qemu-ps2-input-probe-check.ps1`, `scripts/baremetal-qemu-ps2-input-baseline-probe-check.ps1`, `scripts/baremetal-qemu-ps2-keyboard-event-payload-probe-check.ps1`, `scripts/baremetal-qemu-ps2-keyboard-modifier-queue-probe-check.ps1`, `scripts/baremetal-qemu-ps2-mouse-accumulator-state-probe-check.ps1`, and `scripts/baremetal-qemu-ps2-mouse-packet-payload-probe-check.ps1`) now fail directly on the mailbox baseline, keyboard payloads, modifier/queue state, mouse accumulator state, and mouse packet payload invariants over the freestanding PVH artifact. - `FS5.5` storage/disk strict closure is now reached locally: `src/baremetal/storage_backend.zig` provides the shared backend selector, `src/baremetal/ata_pio_disk.zig` provides a real ATA PIO `IDENTIFY` / `READ` / `WRITE` / `FLUSH` path plus bounded multi-partition MBR/GPT discovery/export, first-usable-MBR and protective-MBR GPT partition mounting with logical LBA translation, `src/pal/storage.zig` plus `src/baremetal/tool_layout.zig` route through that shared backend, `src/pal/storage.zig` now also exports logical base-LBA plus bounded partition count/info/select on the mounted storage view, `src/baremetal_main.zig` exposes that same partition-aware storage seam through the `oc_storage_*` ABI exports, partition selection now invalidates stale tool-layout/filesystem state and is paired with explicit `oc_tool_layout_format` plus `oc_filesystem_format` control on the selected partition, `src/baremetal/disk_installer.zig` seeds the canonical persisted install layout (`/boot`, `/system`, `/runtime/install`, bootstrap package) on the active backend, hosted/host regressions prove backend preference, identify-backed capacity, ATA mock-device read/write/flush behavior, multi-partition MBR/GPT discovery plus explicit selection, logical base-LBA translation, installer-layout persistence, ATA-backed export reporting, direct `oc_storage_*` partition export/selection behavior, rebind-safe tool-layout/filesystem invalidation, and per-partition persistence after switching between primary and secondary MBR partitions, and the live QEMU proofs `scripts/baremetal-qemu-ata-storage-probe-check.ps1` and `scripts/baremetal-qemu-ata-gpt-installer-probe-check.ps1` now boot real MBR and protective-MBR GPT raw images and prove raw ATA block mutation + readback, secondary-partition export/selection through the exported seam, secondary-partition tool-layout formatting + payload persistence, secondary-partition filesystem formatting + superblock persistence, ATA-backed tool-layout persistence, ATA-backed filesystem persistence, GPT-backed installer layout seeding, and persisted bootstrap package execution over the freestanding PVH artifact. - `FS5.5` Ethernet-driver strict closure is now reached locally: `src/baremetal/rtl8139.zig` provides the real RTL8139 PCI-discovered bring-up, MAC readout, RX ring programming, TX slot programming, and loopback-friendly datapath validation, `src/baremetal/pci.zig` discovers the I/O BAR + IRQ line and enables I/O plus bus mastering on the selected PCI function, `src/pal/net.zig` and `src/baremetal_main.zig` expose the same raw-frame PAL/export seam, host regressions prove mock-device init/send/receive behavior, and the live QEMU proof `scripts/baremetal-qemu-rtl8139-probe-check.ps1` now proves MAC readout, TX, RX loopback, payload validation, and TX/RX counter advance over the freestanding PVH artifact. - `FS5.5` TCP/IP strict closure is now reached locally: `src/protocol/ethernet.zig` and `src/protocol/arp.zig` provide Ethernet/ARP framing plus ARP reply encode/decode, `src/protocol/ipv4.zig` and `src/protocol/udp.zig` provide IPv4/UDP framing plus checksum handling, `src/protocol/tcp.zig` now provides strict TCP framing plus a minimal client/server session state machine for `SYN -> SYN-ACK -> ACK`, established payload exchange, bounded four-way teardown, bounded SYN/payload/FIN retransmission recovery, bounded multi-flow session-table management, bounded cumulative-ACK advancement across multiple in-flight payload chunks, strict remote-window enforcement for bounded sequential payload chunking, zero-window blocking until a pure ACK reopens the remote window, and bounded sender congestion-window growth after ACK plus payload-timeout collapse on the chunked send path, `src/protocol/dhcp.zig` now provides strict DHCP discover encode/decode, `src/protocol/dns.zig` now provides strict DNS query and A-response encode/decode plus caller-owned decode storage on the freestanding path, `src/pal/tls_client_light.zig` now provides the bounded freestanding TLS client used by the PAL network path plus precise last-certificate-error reporting, `src/pal/net.zig` exposes `sendArpRequest` / `pollArpPacket`, `sendIpv4Frame` / `pollIpv4PacketStrict`, `sendUdpPacket` / `pollUdpPacketStrictInto`, `sendTcpPacket` / `pollTcpPacketStrictInto`, `sendDnsQuery` / `pollDnsPacketStrictInto`, DHCP send/poll helpers, routed networking helpers (`configureIpv4Route`, `configureIpv4RouteFromDhcp`, `resolveNextHop`, `learnArpPacket`, `sendUdpPacketRouted`), explicit DNS server configuration (`configureDnsServers`, `configureDnsServersFromDhcp`), a real freestanding bounded `http://` POST path, and a real freestanding bounded `https://` POST transport path with deterministic filesystem-backed CA-bundle verification, `src/baremetal/tool_service.zig` now exposes the bounded typed framed request/response shim used by the bare-metal TCP proof with `CMD` / `EXEC` / `GET` / `PUT` / `STAT` / `LIST` / `INSTALL` / `MANIFEST` / `PKG` / `PKGLIST` / `PKGINFO` / `PKGRUN` / `PKGAPP` / `PKGDISPLAY` / `PKGPUT` / `PKGLS` / `PKGGET` / `PKGDELETE` / `APPLIST` / `APPINFO` / `APPSTATE` / `APPHISTORY` / `APPSTDOUT` / `APPSTDERR` / `APPTRUST` / `APPCONNECTOR` / `APPRUN` / `APPDELETE` / `DISPLAYINFO` / `DISPLAYMODES` / `DISPLAYSET` / `TRUSTPUT` / `TRUSTLIST` / `TRUSTINFO` / `TRUSTACTIVE` / `TRUSTSELECT` / `TRUSTDELETE` plus bounded batched request parsing/execution on one flow, host regressions prove ARP, IPv4, UDP, TCP handshake/payload, bounded four-way close, dropped-first-SYN retransmission recovery, dropped-first-payload retransmission recovery, dropped-first-FIN retransmission recovery on both close sides, bounded multi-flow session isolation, bounded cumulative-ACK advancement through multiple in-flight chunks, bounded sender congestion-window growth/collapse on the chunked send path, DHCP, DNS, DHCP-driven route configuration, gateway ARP learning, routed off-subnet UDP delivery, direct-subnet UDP bypass, zero-window block/reopen, bounded sequential payload chunking, framed multi-request command-service exchange, structured `EXEC` service behavior, bounded typed batch request multiplexing, typed `PUT`/`GET`/`STAT`/`LIST` service behavior, typed `INSTALL` / `MANIFEST` runtime-layout service behavior, typed `PKG` / `PKGLIST` / `PKGINFO` / `PKGRUN` / `PKGAPP` / `PKGDISPLAY` / `PKGPUT` / `PKGLS` / `PKGGET` / `PKGDELETE` package-service behavior, typed `APPLIST` / `APPINFO` / `APPSTATE` / `APPHISTORY` / `APPSTDOUT` / `APPSTDERR` / `APPTRUST` / `APPCONNECTOR` / `APPRUN` / `APPDELETE` app-lifecycle behavior, typed `DISPLAYINFO` / `DISPLAYMODES` / `DISPLAYSET` display behavior, typed `TRUSTPUT` / `TRUSTLIST` / `TRUSTINFO` / `TRUSTACTIVE` / `TRUSTSELECT` / `TRUSTDELETE` trust-store behavior, persisted `run-script` execution over the mock RTL8139 device, hostname-resolved plain-HTTP POST/response exchange over the freestanding PAL network path, bundle-trust configuration from an embedded root, and TLS `ClientHello` emission through the same mock RTL8139 seam, the PVH boot stack was expanded to `128 KiB` to cover the real DNS + TCP + HTTP + HTTPS + service path, and the live QEMU proofs `scripts/baremetal-qemu-rtl8139-arp-probe-check.ps1`, `scripts/baremetal-qemu-rtl8139-ipv4-probe-check.ps1`, `scripts/baremetal-qemu-rtl8139-udp-probe-check.ps1`, `scripts/baremetal-qemu-rtl8139-tcp-probe-check.ps1`, `scripts/baremetal-qemu-rtl8139-dhcp-probe-check.ps1`, `scripts/baremetal-qemu-rtl8139-dns-probe-check.ps1`, `scripts/baremetal-qemu-rtl8139-gateway-probe-check.ps1`, `scripts/baremetal-qemu-rtl8139-http-post-probe-check.ps1`, and `scripts/baremetal-qemu-rtl8139-https-post-probe-check.ps1` now prove ARP, IPv4, UDP, TCP handshake/payload exchange, bounded four-way close, bounded SYN/payload/FIN retransmission recovery, bounded two-flow session isolation, zero-window block/reopen, bounded sequential payload chunking, bounded sender congestion-window growth after ACK plus payload-timeout collapse, framed multi-request command-service exchange, structured `EXEC` request/response exchange, bounded typed batch request multiplexing on one flow with concatenated framed responses, typed TCP `PUT` upload with direct filesystem readback over attached disk media, typed `INSTALL` / `MANIFEST` runtime-layout service exchange with `/boot/loader.cfg` readback, typed TCP `PKG` / `PKGLIST` / `PKGINFO` / `PKGRUN` / `PKGAPP` / `PKGDISPLAY` package-service exchange, typed `PKGPUT` / `PKGLS` / `PKGGET` / `PKGDELETE` package-asset and uninstall exchange, typed `APPLIST` / `APPINFO` / `APPSTATE` / `APPHISTORY` / `APPSTDOUT` / `APPSTDERR` / `APPTRUST` / `APPCONNECTOR` / `APPRUN` / `APPDELETE` app-lifecycle exchange with persisted runtime-state readback, persisted history-log readback, persisted stdout/stderr readback, and uninstall cleanup, typed `DISPLAYINFO` / `DISPLAYMODES` / `DISPLAYSET` display exchange, typed `TRUSTPUT` / `TRUSTLIST` / `TRUSTINFO` / `TRUSTACTIVE` / `TRUSTSELECT` / `TRUSTDELETE` trust-store exchange, selected trust-bundle query/path readback, trust-bundle deletion, post-delete remaining-list readback, package manifest readback, package app-manifest readback, package display-profile persistence, package-directory listing, package output readback, live `run-package` display-mode application, explicit `DISPLAYSET` mode change/readback, DHCP discover framing/decode, DNS query/A-response transport, ARP-cache learning, gateway-routed UDP delivery, direct-subnet gateway bypass, hostname-resolved plain-HTTP POST/response exchange, and live TLS-backed HTTPS request/response exchange against a deterministic self-hosted harness over direct-IP transport with fixed probe time and filesystem-backed CA-bundle trust, and TX/RX counter advance over the freestanding PVH artifact. -- `FS5.5` broader runtime/service depth continues to advance above the strict closure bar: the latest slice adds persisted workspace plans under `/runtime/workspace-plans//.txt` plus `/runtime/workspace-plans//active.txt`, new CLI verbs (`workspace-plan-list`, `workspace-plan-info`, `workspace-plan-active`, `workspace-plan-save`, `workspace-plan-apply`, `workspace-plan-delete`), typed TCP verbs (`WORKSPACEPLANLIST`, `WORKSPACEPLANINFO`, `WORKSPACEPLANACTIVE`, `WORKSPACEPLANSAVE`, `WORKSPACEPLANAPPLY`, `WORKSPACEPLANDELETE`), RAM-disk and ATA-backed persistence tests, and a live RTL8139 TCP proof for save -> list -> info -> apply -> active -> restore -> delete with restored suite/trust/display/channel state; current local validation is green at `zig build test --summary all` -> `398/398` passed. +- `FS5.5` broader runtime/service depth continues to advance above the strict closure bar: the latest slice adds persisted workspace plans under `/runtime/workspace-plans//.txt` plus `/runtime/workspace-plans//active.txt`, new CLI verbs (`workspace-plan-list`, `workspace-plan-info`, `workspace-plan-active`, `workspace-plan-save`, `workspace-plan-apply`, `workspace-plan-delete`), typed TCP verbs (`WORKSPACEPLANLIST`, `WORKSPACEPLANINFO`, `WORKSPACEPLANACTIVE`, `WORKSPACEPLANSAVE`, `WORKSPACEPLANAPPLY`, `WORKSPACEPLANDELETE`), RAM-disk and ATA-backed persistence tests, and a live RTL8139 TCP proof for save -> list -> info -> apply -> active -> restore -> delete with restored suite/trust/display/channel state; current local validation is green at `zig build test --summary all` -> `399/399` passed. - `FS5.5` filesystem usage strict closure is now reached locally: `src/baremetal/filesystem.zig` now provides a real path-based filesystem layer above the shared storage backend, `src/pal/fs.zig` routes the freestanding PAL through that layer, `src/baremetal_main.zig` exports filesystem state/entries, and both hosted + bare-metal host regressions prove path-based persistence over RAM-disk and ATA PIO (`/runtime/state/agent.json`, `/tools/cache/tool.txt`, `/tools/scripts/bootstrap.oc`, `/tools/script/output.txt`). - `FS5.5` bare-metal tool execution strict closure is now reached locally: `src/baremetal/tool_exec.zig` now provides the real freestanding builtin command substrate including persisted `run-script` execution, canonical `run-package`, `package-verify`, `package-app`, `package-display`, `package-ls`, `package-cat`, `app-list`, `app-info`, `app-state`, `app-history`, `app-stdout`, `app-stderr`, `app-trust`, `app-connector`, `app-run`, `display-info`, `display-modes`, and `display-set`, `src/baremetal/package_store.zig` now provides the canonical persisted package layout under `/packages//bin/main.oc`, `/packages//meta/package.txt`, `/packages//meta/app.txt`, and `/packages//assets/...` plus manifest `script_checksum`, `app_manifest_checksum`, and `asset_tree_checksum` fields, `src/pal/proc.zig` exposes explicit freestanding capture through `runCaptureFreestanding(...)`, `src/baremetal/tool_service.zig` now exposes the bounded typed request/response shim used by the bare-metal TCP proof including typed `PKGVERIFY`, the storage/filesystem dependency chain closes through `src/baremetal/filesystem.zig`, `src/pal/fs.zig`, and the shared storage backend, host/module validation proves the builtin dispatch path plus typed TCP file-service behavior, typed package-service behavior, typed package-app/package-display service behavior, typed app-lifecycle service behavior, typed app stdout/stderr service behavior, typed package-asset and display-query/control behavior, ATA-backed package persistence, persisted package display profiles, deterministic package-integrity mismatch reporting on script tamper via `field=script_checksum`, persisted app runtime-state receipts, persisted app stdout/stderr receipts, and canonical `run-package` / `app-run` execution on top of the same filesystem layer, and the live QEMU proof `scripts/baremetal-qemu-tool-exec-probe-check.ps1` now proves `help`, `mkdir`, `write-file`, `cat`, `stat`, `run-script`, direct filesystem readback, persisted script readback after filesystem reset/re-init, and `echo` over the freestanding PVH artifact with attached disk media while the live RTL8139 TCP proof now covers persisted app-state and stdout/stderr readback plus the typed `PKGVERIFY` success receipt on the live package tree. - Latest FS5.5 autorun slice: `src/baremetal/app_runtime.zig` now persists `/runtime/apps/autorun.txt`, `src/baremetal/tool_exec.zig` now exposes `app-autorun-list`, `app-autorun-add`, `app-autorun-remove`, and `app-autorun-run`, `src/baremetal/tool_service.zig` now exposes `APPAUTORUNLIST`, `APPAUTORUNADD`, `APPAUTORUNREMOVE`, and `APPAUTORUNRUN`, host/module validation proves RAM-disk and ATA-backed autorun registry persistence plus autorun execution receipts, `src/baremetal/filesystem.zig` now carries a `128`-entry filesystem budget so the deeper FS5.5 package/trust/app/autorun/workspace runtime state fits on the persisted surface without live-service `NoSpace` failures, and the live RTL8139 TCP proof now covers autorun add/list/run/remove plus `/runtime/apps/autorun.txt`, `/runtime/apps/aux/last_run.txt`, and `/runtime/apps/aux/stdout.log` readback. diff --git a/docs/zig-port/PORT_PLAN.md b/docs/zig-port/PORT_PLAN.md index d7f68b9a..c186712f 100644 --- a/docs/zig-port/PORT_PLAN.md +++ b/docs/zig-port/PORT_PLAN.md @@ -17,10 +17,10 @@ Full-stack replacement execution reference: - bounded `640x400x32bpp`, `800x600x32bpp`, `1024x768x32bpp`, `1280x720x32bpp`, and `1280x1024x32bpp` framebuffer layouts - glyph rendering into the hardware-backed MMIO surface - structured PCI display-adapter discovery shipped in `src/baremetal/pci.zig` and the PAL surface is exposed in `src/pal/framebuffer.zig`, with bounded mode switching plus supported-mode enumeration exported through `oc_framebuffer_set_mode`, `oc_framebuffer_supported_mode_count`, `oc_framebuffer_supported_mode_width`, and `oc_framebuffer_supported_mode_height`. - - `src/baremetal/edid.zig`, `src/baremetal/display_output.zig`, and `src/baremetal/virtio_gpu.zig` now add the first real EDID-backed controller-capability path over `virtio-gpu-pci`, with exported display-output state and EDID bytes routed through `src/pal/framebuffer.zig` and the bare-metal ABI. - - hosted/host regressions now prove framebuffer state, display-output state, adapter metadata, supported-mode enumeration, glyph pixel updates, bounded mode switching, and preservation of the last valid mode on unsupported requests. + - `src/baremetal/edid.zig`, `src/baremetal/display_output.zig`, and `src/baremetal/virtio_gpu.zig` now add the first real EDID-backed controller-capability path over `virtio-gpu-pci`, with exported display-output state, bounded per-output entry export, and EDID bytes routed through `src/pal/framebuffer.zig` and the bare-metal ABI. + - hosted/host regressions now prove framebuffer state, display-output state, output-entry metadata, adapter metadata, supported-mode enumeration, glyph pixel updates, bounded mode switching, and preservation of the last valid mode on unsupported requests. - live QEMU+GDB proof `scripts/baremetal-qemu-framebuffer-console-probe-check.ps1` reads back real MMIO banner pixels plus exported adapter metadata from the hardware-backed framebuffer BAR over the freestanding PVH artifact at `640x400`, `1024x768`, and `1280x720`. - - live QEMU+GDB proof `scripts/baremetal-qemu-virtio-gpu-display-probe-check.ps1` reads back real `virtio-gpu-pci` EDID/controller capability state, including scanout geometry, physical size, manufacturer/product IDs, and EDID bytes. + - live QEMU+GDB proof `scripts/baremetal-qemu-virtio-gpu-display-probe-check.ps1` reads back real `virtio-gpu-pci` EDID/controller capability state, including scanout geometry, physical size, manufacturer/product IDs, EDID bytes, and the exported output-entry metadata for the selected scanout. - real HDMI/DisplayPort connector-specific scanout paths are still future depth and are not claimed by the current branch. - keyboard/mouse strict closure is now reached locally. - real PS/2 controller path shipped in `src/baremetal/ps2_input.zig`: diff --git a/scripts/baremetal-qemu-virtio-gpu-display-probe-check.ps1 b/scripts/baremetal-qemu-virtio-gpu-display-probe-check.ps1 index 40cb967b..d21a67cf 100644 --- a/scripts/baremetal-qemu-virtio-gpu-display-probe-check.ps1 +++ b/scripts/baremetal-qemu-virtio-gpu-display-probe-check.ps1 @@ -24,6 +24,7 @@ $expectedHeight = 800 $expectedMinEdidLength = 128 $expectedCapabilityDigital = 0x0001 $expectedCapabilityPreferredTiming = 0x0002 +$expectedOutputEntryCount = 1 $stateMagicOffset = 0 $stateApiVersionOffset = 4 @@ -52,6 +53,16 @@ $stateSerialNumberOffset = 40 $stateEdidLengthOffset = 44 $stateCapabilityFlagsOffset = 46 +$outputEntryConnectedOffset = 0 +$outputEntryScanoutIndexOffset = 1 +$outputEntryConnectorOffset = 2 +$outputEntryEdidPresentOffset = 3 +$outputEntryCurrentWidthOffset = 4 +$outputEntryCurrentHeightOffset = 6 +$outputEntryPreferredWidthOffset = 8 +$outputEntryPreferredHeightOffset = 10 +$outputEntryCapabilityFlagsOffset = 20 + function Resolve-ZigExecutable { $default = "C:\Users\Ady\Documents\toolchains\zig-master\current\zig.exe" if ($env:OPENCLAW_ZIG_BIN -and $env:OPENCLAW_ZIG_BIN.Trim().Length -gt 0) { @@ -240,6 +251,8 @@ if ($LASTEXITCODE -ne 0 -or $null -eq $symbolOutput -or $symbolOutput.Count -eq $qemuExitAddress = Resolve-SymbolAddress -SymbolLines $symbolOutput -Pattern '\s[tT]\sbaremetal_main\.qemuExit$' -SymbolName "baremetal_main.qemuExit" $displayStateAddress = Resolve-SymbolAddress -SymbolLines $symbolOutput -Pattern '\s[dDbB]\sbaremetal\.display_output\.state$' -SymbolName "baremetal.display_output.state" $edidBytesAddress = Resolve-SymbolAddress -SymbolLines $symbolOutput -Pattern '\s[dDbB]\sbaremetal\.display_output\.edid_bytes$' -SymbolName "baremetal.display_output.edid_bytes" +$outputEntryCountAddress = Resolve-SymbolAddress -SymbolLines $symbolOutput -Pattern '\s[dDbB]\soc_display_output_entry_count_data$' -SymbolName "oc_display_output_entry_count_data" +$outputEntriesAddress = Resolve-SymbolAddress -SymbolLines $symbolOutput -Pattern '\s[dDbB]\soc_display_output_entries_data$' -SymbolName "oc_display_output_entries_data" $artifactForGdb = $artifact.Replace('\', '/') Remove-PathWithRetry $gdbStdout @@ -286,6 +299,16 @@ commands printf "DISPLAY_SERIAL_NUMBER=%u\n", *(unsigned int*)(__DISPLAY_STATE_ADDR__ + __SERIAL_NUMBER_OFFSET__) printf "DISPLAY_EDID_LENGTH=%u\n", *(unsigned short*)(__DISPLAY_STATE_ADDR__ + __EDID_LENGTH_OFFSET__) printf "DISPLAY_CAPABILITY_FLAGS=%u\n", *(unsigned short*)(__DISPLAY_STATE_ADDR__ + __CAPABILITY_FLAGS_OFFSET__) + printf "DISPLAY_OUTPUT_ENTRY_COUNT=%u\n", *(unsigned short*)(__OUTPUT_ENTRY_COUNT_ADDR__) + printf "DISPLAY_OUTPUT0_CONNECTED=%u\n", *(unsigned char*)(__OUTPUT_ENTRIES_ADDR__ + __OUTPUT_ENTRY_CONNECTED_OFFSET__) + printf "DISPLAY_OUTPUT0_SCANOUT=%u\n", *(unsigned char*)(__OUTPUT_ENTRIES_ADDR__ + __OUTPUT_ENTRY_SCANOUT_OFFSET__) + printf "DISPLAY_OUTPUT0_CONNECTOR=%u\n", *(unsigned char*)(__OUTPUT_ENTRIES_ADDR__ + __OUTPUT_ENTRY_CONNECTOR_OFFSET__) + printf "DISPLAY_OUTPUT0_EDID_PRESENT=%u\n", *(unsigned char*)(__OUTPUT_ENTRIES_ADDR__ + __OUTPUT_ENTRY_EDID_PRESENT_OFFSET__) + printf "DISPLAY_OUTPUT0_CURRENT_WIDTH=%u\n", *(unsigned short*)(__OUTPUT_ENTRIES_ADDR__ + __OUTPUT_ENTRY_CURRENT_WIDTH_OFFSET__) + printf "DISPLAY_OUTPUT0_CURRENT_HEIGHT=%u\n", *(unsigned short*)(__OUTPUT_ENTRIES_ADDR__ + __OUTPUT_ENTRY_CURRENT_HEIGHT_OFFSET__) + printf "DISPLAY_OUTPUT0_PREFERRED_WIDTH=%u\n", *(unsigned short*)(__OUTPUT_ENTRIES_ADDR__ + __OUTPUT_ENTRY_PREFERRED_WIDTH_OFFSET__) + printf "DISPLAY_OUTPUT0_PREFERRED_HEIGHT=%u\n", *(unsigned short*)(__OUTPUT_ENTRIES_ADDR__ + __OUTPUT_ENTRY_PREFERRED_HEIGHT_OFFSET__) + printf "DISPLAY_OUTPUT0_CAPABILITY_FLAGS=%u\n", *(unsigned short*)(__OUTPUT_ENTRIES_ADDR__ + __OUTPUT_ENTRY_CAPABILITY_FLAGS_OFFSET__) printf "DISPLAY_EDID_0=%u\n", *(unsigned char*)(__EDID_BYTES_ADDR__ + 0) printf "DISPLAY_EDID_1=%u\n", *(unsigned char*)(__EDID_BYTES_ADDR__ + 1) printf "DISPLAY_EDID_2=%u\n", *(unsigned char*)(__EDID_BYTES_ADDR__ + 2) @@ -305,6 +328,8 @@ $gdbScriptContent = $gdbTemplate ` -replace '__QEMU_EXIT__', $qemuExitAddress ` -replace '__DISPLAY_STATE_ADDR__', ('0x' + $displayStateAddress) ` -replace '__EDID_BYTES_ADDR__', ('0x' + $edidBytesAddress) ` + -replace '__OUTPUT_ENTRY_COUNT_ADDR__', ('0x' + $outputEntryCountAddress) ` + -replace '__OUTPUT_ENTRIES_ADDR__', ('0x' + $outputEntriesAddress) ` -replace '__MAGIC_OFFSET__', $stateMagicOffset ` -replace '__API_VERSION_OFFSET__', $stateApiVersionOffset ` -replace '__BACKEND_OFFSET__', $stateBackendOffset ` @@ -330,7 +355,16 @@ $gdbScriptContent = $gdbTemplate ` -replace '__PRODUCT_CODE_OFFSET__', $stateProductCodeOffset ` -replace '__SERIAL_NUMBER_OFFSET__', $stateSerialNumberOffset ` -replace '__EDID_LENGTH_OFFSET__', $stateEdidLengthOffset ` - -replace '__CAPABILITY_FLAGS_OFFSET__', $stateCapabilityFlagsOffset + -replace '__CAPABILITY_FLAGS_OFFSET__', $stateCapabilityFlagsOffset ` + -replace '__OUTPUT_ENTRY_CONNECTED_OFFSET__', $outputEntryConnectedOffset ` + -replace '__OUTPUT_ENTRY_SCANOUT_OFFSET__', $outputEntryScanoutIndexOffset ` + -replace '__OUTPUT_ENTRY_CONNECTOR_OFFSET__', $outputEntryConnectorOffset ` + -replace '__OUTPUT_ENTRY_EDID_PRESENT_OFFSET__', $outputEntryEdidPresentOffset ` + -replace '__OUTPUT_ENTRY_CURRENT_WIDTH_OFFSET__', $outputEntryCurrentWidthOffset ` + -replace '__OUTPUT_ENTRY_CURRENT_HEIGHT_OFFSET__', $outputEntryCurrentHeightOffset ` + -replace '__OUTPUT_ENTRY_PREFERRED_WIDTH_OFFSET__', $outputEntryPreferredWidthOffset ` + -replace '__OUTPUT_ENTRY_PREFERRED_HEIGHT_OFFSET__', $outputEntryPreferredHeightOffset ` + -replace '__OUTPUT_ENTRY_CAPABILITY_FLAGS_OFFSET__', $outputEntryCapabilityFlagsOffset $gdbScriptContent | Set-Content -Path $gdbScript -Encoding Ascii $qemuProcess = $null @@ -403,6 +437,16 @@ $productCode = Extract-IntValue -Text $out -Name 'DISPLAY_PRODUCT_CODE' $serialNumber = Extract-IntValue -Text $out -Name 'DISPLAY_SERIAL_NUMBER' $edidLength = Extract-IntValue -Text $out -Name 'DISPLAY_EDID_LENGTH' $capabilityFlags = Extract-IntValue -Text $out -Name 'DISPLAY_CAPABILITY_FLAGS' +$outputEntryCount = Extract-IntValue -Text $out -Name 'DISPLAY_OUTPUT_ENTRY_COUNT' +$output0Connected = Extract-IntValue -Text $out -Name 'DISPLAY_OUTPUT0_CONNECTED' +$output0Scanout = Extract-IntValue -Text $out -Name 'DISPLAY_OUTPUT0_SCANOUT' +$output0Connector = Extract-IntValue -Text $out -Name 'DISPLAY_OUTPUT0_CONNECTOR' +$output0EdidPresent = Extract-IntValue -Text $out -Name 'DISPLAY_OUTPUT0_EDID_PRESENT' +$output0CurrentWidth = Extract-IntValue -Text $out -Name 'DISPLAY_OUTPUT0_CURRENT_WIDTH' +$output0CurrentHeight = Extract-IntValue -Text $out -Name 'DISPLAY_OUTPUT0_CURRENT_HEIGHT' +$output0PreferredWidth = Extract-IntValue -Text $out -Name 'DISPLAY_OUTPUT0_PREFERRED_WIDTH' +$output0PreferredHeight = Extract-IntValue -Text $out -Name 'DISPLAY_OUTPUT0_PREFERRED_HEIGHT' +$output0CapabilityFlags = Extract-IntValue -Text $out -Name 'DISPLAY_OUTPUT0_CAPABILITY_FLAGS' $edid0 = Extract-IntValue -Text $out -Name 'DISPLAY_EDID_0' $edid1 = Extract-IntValue -Text $out -Name 'DISPLAY_EDID_1' $edid2 = Extract-IntValue -Text $out -Name 'DISPLAY_EDID_2' @@ -448,6 +492,16 @@ Write-Output "BAREMETAL_QEMU_VIRTIO_GPU_DISPLAY_PROBE_MANUFACTURER_ID=$manufactu Write-Output "BAREMETAL_QEMU_VIRTIO_GPU_DISPLAY_PROBE_PRODUCT_CODE=$productCode" Write-Output "BAREMETAL_QEMU_VIRTIO_GPU_DISPLAY_PROBE_SERIAL_NUMBER=$serialNumber" Write-Output "BAREMETAL_QEMU_VIRTIO_GPU_DISPLAY_PROBE_EDID_LENGTH=$edidLength" +Write-Output "BAREMETAL_QEMU_VIRTIO_GPU_DISPLAY_PROBE_OUTPUT_ENTRY_COUNT=$outputEntryCount" +Write-Output "BAREMETAL_QEMU_VIRTIO_GPU_DISPLAY_PROBE_OUTPUT0_CONNECTED=$output0Connected" +Write-Output "BAREMETAL_QEMU_VIRTIO_GPU_DISPLAY_PROBE_OUTPUT0_SCANOUT=$output0Scanout" +Write-Output "BAREMETAL_QEMU_VIRTIO_GPU_DISPLAY_PROBE_OUTPUT0_CONNECTOR=$output0Connector" +Write-Output "BAREMETAL_QEMU_VIRTIO_GPU_DISPLAY_PROBE_OUTPUT0_EDID_PRESENT=$output0EdidPresent" +Write-Output "BAREMETAL_QEMU_VIRTIO_GPU_DISPLAY_PROBE_OUTPUT0_CURRENT_WIDTH=$output0CurrentWidth" +Write-Output "BAREMETAL_QEMU_VIRTIO_GPU_DISPLAY_PROBE_OUTPUT0_CURRENT_HEIGHT=$output0CurrentHeight" +Write-Output "BAREMETAL_QEMU_VIRTIO_GPU_DISPLAY_PROBE_OUTPUT0_PREFERRED_WIDTH=$output0PreferredWidth" +Write-Output "BAREMETAL_QEMU_VIRTIO_GPU_DISPLAY_PROBE_OUTPUT0_PREFERRED_HEIGHT=$output0PreferredHeight" +Write-Output "BAREMETAL_QEMU_VIRTIO_GPU_DISPLAY_PROBE_OUTPUT0_CAPABILITY_FLAGS=$output0CapabilityFlags" $pass = ( $magic -eq $displayMagic -and @@ -474,6 +528,16 @@ $pass = ( $manufacturerId -gt 0 -and $productCode -gt 0 -and $edidLength -ge $expectedMinEdidLength -and + $outputEntryCount -eq $expectedOutputEntryCount -and + $output0Connected -eq 1 -and + $output0Scanout -eq 0 -and + $output0Connector -eq $expectedConnector -and + $output0EdidPresent -eq 1 -and + $output0CurrentWidth -eq $expectedWidth -and + $output0CurrentHeight -eq $expectedHeight -and + $output0PreferredWidth -gt 0 -and + $output0PreferredHeight -gt 0 -and + $output0CapabilityFlags -eq $capabilityFlags -and ($capabilityFlags -band $expectedCapabilityDigital) -ne 0 -and ($capabilityFlags -band $expectedCapabilityPreferredTiming) -ne 0 -and $edid0 -eq 0 -and diff --git a/src/baremetal/abi.zig b/src/baremetal/abi.zig index a71058d5..e128d920 100644 --- a/src/baremetal/abi.zig +++ b/src/baremetal/abi.zig @@ -373,6 +373,24 @@ pub const BaremetalDisplayOutputState = extern struct { capability_flags: u16, }; +pub const BaremetalDisplayOutputEntry = extern struct { + connected: u8, + scanout_index: u8, + connector_type: u8, + edid_present: u8, + current_width: u16, + current_height: u16, + preferred_width: u16, + preferred_height: u16, + physical_width_mm: u16, + physical_height_mm: u16, + manufacturer_id: u16, + product_code: u16, + capability_flags: u16, + edid_length: u16, + serial_number: u32, +}; + pub const BaremetalStorageState = extern struct { magic: u32, api_version: u16, diff --git a/src/baremetal/display_output.zig b/src/baremetal/display_output.zig index 9eb19b8b..e1c5dfb5 100644 --- a/src/baremetal/display_output.zig +++ b/src/baremetal/display_output.zig @@ -3,6 +3,8 @@ const std = @import("std"); const abi = @import("abi.zig"); pub const max_edid_bytes: usize = 1024; +pub const max_output_entries: usize = 16; +pub const OutputEntry = abi.BaremetalDisplayOutputEntry; pub const BgaUpdate = struct { vendor_id: u16 = 0, @@ -37,10 +39,56 @@ pub const VirtioGpuUpdate = struct { serial_number: u32, capability_flags: u16, edid: []const u8, + scanouts: []const VirtioGpuScanoutUpdate = &.{}, +}; + +pub const VirtioGpuScanoutUpdate = struct { + connected: bool, + scanout_index: u8, + current_width: u16, + current_height: u16, + preferred_width: u16, + preferred_height: u16, + physical_width_mm: u16, + physical_height_mm: u16, + manufacturer_id: u16, + product_code: u16, + serial_number: u32, + capability_flags: u16, + edid_length: u16, }; var state: abi.BaremetalDisplayOutputState = undefined; var edid_bytes: [max_edid_bytes]u8 = [_]u8{0} ** max_edid_bytes; +pub export var oc_display_output_entry_count_data: u16 = 0; +pub export var oc_display_output_entries_data: [max_output_entries]OutputEntry = [_]OutputEntry{zeroOutputEntry()} ** max_output_entries; + +fn zeroOutputEntry() OutputEntry { + return .{ + .connected = 0, + .scanout_index = 0, + .connector_type = abi.display_connector_none, + .edid_present = 0, + .current_width = 0, + .current_height = 0, + .preferred_width = 0, + .preferred_height = 0, + .physical_width_mm = 0, + .physical_height_mm = 0, + .manufacturer_id = 0, + .product_code = 0, + .capability_flags = 0, + .edid_length = 0, + .serial_number = 0, + }; +} + +fn clearOutputEntries() void { + oc_display_output_entry_count_data = 0; + for (&oc_display_output_entries_data) |*entry| { + entry.* = zeroOutputEntry(); + } +} fn initState() void { state = .{ @@ -72,6 +120,7 @@ fn initState() void { .edid_length = 0, .capability_flags = 0, }; + clearOutputEntries(); } pub fn resetForTest() void { @@ -83,6 +132,16 @@ pub fn statePtr() *const abi.BaremetalDisplayOutputState { return &state; } +pub fn outputCount() u16 { + return oc_display_output_entry_count_data; +} + +pub fn outputEntry(index: u16) OutputEntry { + const idx: usize = @intCast(index); + if (idx >= oc_display_output_entry_count_data or idx >= oc_display_output_entries_data.len) return zeroOutputEntry(); + return oc_display_output_entries_data[idx]; +} + pub fn edidByte(index: u16) u8 { const idx: usize = @intCast(index); if (idx >= state.edid_length or idx >= edid_bytes.len) return 0; @@ -107,6 +166,24 @@ pub fn updateFromBga(update: BgaUpdate) void { state.current_height = update.height; state.preferred_width = update.width; state.preferred_height = update.height; + oc_display_output_entry_count_data = 1; + oc_display_output_entries_data[0] = .{ + .connected = if (update.connected) 1 else 0, + .scanout_index = 0, + .connector_type = abi.display_connector_virtual, + .edid_present = 0, + .current_width = update.width, + .current_height = update.height, + .preferred_width = update.width, + .preferred_height = update.height, + .physical_width_mm = 0, + .physical_height_mm = 0, + .manufacturer_id = 0, + .product_code = 0, + .capability_flags = 0, + .edid_length = 0, + .serial_number = 0, + }; } pub fn inferConnectorType(capability_flags: u16) u8 { @@ -151,6 +228,50 @@ pub fn updateFromVirtioGpu(update: VirtioGpuUpdate) void { std.mem.copyForwards(u8, edid_bytes[0..edid_len], update.edid[0..edid_len]); } state.edid_length = @intCast(edid_len); + + const scanout_len = @min(update.scanouts.len, oc_display_output_entries_data.len); + if (scanout_len == 0) { + oc_display_output_entry_count_data = 1; + oc_display_output_entries_data[0] = .{ + .connected = if (update.connected) 1 else 0, + .scanout_index = update.active_scanout, + .connector_type = state.connector_type, + .edid_present = state.edid_present, + .current_width = update.current_width, + .current_height = update.current_height, + .preferred_width = update.preferred_width, + .preferred_height = update.preferred_height, + .physical_width_mm = update.physical_width_mm, + .physical_height_mm = update.physical_height_mm, + .manufacturer_id = update.manufacturer_id, + .product_code = update.product_code, + .capability_flags = update.capability_flags, + .edid_length = @intCast(edid_len), + .serial_number = update.serial_number, + }; + return; + } + + oc_display_output_entry_count_data = @intCast(scanout_len); + for (update.scanouts[0..scanout_len], 0..) |scanout, index| { + oc_display_output_entries_data[index] = .{ + .connected = if (scanout.connected) 1 else 0, + .scanout_index = scanout.scanout_index, + .connector_type = if (scanout.connected) inferConnectorType(scanout.capability_flags) else abi.display_connector_none, + .edid_present = if (scanout.edid_length > 0) 1 else 0, + .current_width = scanout.current_width, + .current_height = scanout.current_height, + .preferred_width = scanout.preferred_width, + .preferred_height = scanout.preferred_height, + .physical_width_mm = scanout.physical_width_mm, + .physical_height_mm = scanout.physical_height_mm, + .manufacturer_id = scanout.manufacturer_id, + .product_code = scanout.product_code, + .capability_flags = scanout.capability_flags, + .edid_length = scanout.edid_length, + .serial_number = scanout.serial_number, + }; + } } test "display output state updates from bga metadata" { @@ -174,6 +295,11 @@ test "display output state updates from bga metadata" { try std.testing.expectEqual(@as(u8, 1), output.connected); try std.testing.expectEqual(@as(u16, 1280), output.current_width); try std.testing.expectEqual(@as(u16, 720), output.current_height); + try std.testing.expectEqual(@as(u16, 1), outputCount()); + const entry = outputEntry(0); + try std.testing.expectEqual(@as(u8, 1), entry.connected); + try std.testing.expectEqual(@as(u8, abi.display_connector_virtual), entry.connector_type); + try std.testing.expectEqual(@as(u16, 1280), entry.current_width); } test "display output state copies virtio gpu edid payload" { @@ -200,6 +326,23 @@ test "display output state copies virtio gpu edid payload" { .serial_number = 0xCAFEBABE, .capability_flags = abi.display_capability_digital_input | abi.display_capability_preferred_timing, .edid = &edid, + .scanouts = &.{ + .{ + .connected = true, + .scanout_index = 0, + .current_width = 1280, + .current_height = 800, + .preferred_width = 1280, + .preferred_height = 800, + .physical_width_mm = 300, + .physical_height_mm = 190, + .manufacturer_id = 0x1234, + .product_code = 0x5678, + .serial_number = 0xCAFEBABE, + .capability_flags = abi.display_capability_digital_input | abi.display_capability_preferred_timing, + .edid_length = edid.len, + }, + }, }); const output = statePtr(); try std.testing.expectEqual(@as(u8, abi.display_backend_virtio_gpu), output.backend); @@ -208,6 +351,11 @@ test "display output state copies virtio gpu edid payload" { try std.testing.expectEqual(@as(u16, 4), output.edid_length); try std.testing.expectEqual(@as(u16, abi.display_capability_digital_input | abi.display_capability_preferred_timing), output.capability_flags); try std.testing.expectEqual(@as(u8, 0xFF), edidByte(1)); + try std.testing.expectEqual(@as(u16, 1), outputCount()); + const entry = outputEntry(0); + try std.testing.expectEqual(@as(u8, 0), entry.scanout_index); + try std.testing.expectEqual(@as(u8, abi.display_connector_virtual), entry.connector_type); + try std.testing.expectEqual(@as(u16, 1280), entry.current_width); } test "display output infers connector type from edid capability flags" { @@ -215,3 +363,71 @@ test "display output infers connector type from edid capability flags" { try std.testing.expectEqual(@as(u8, abi.display_connector_displayport), inferConnectorType(abi.display_capability_displayid_extension)); try std.testing.expectEqual(@as(u8, abi.display_connector_virtual), inferConnectorType(abi.display_capability_digital_input)); } + +test "display output state stores multiple virtio scanout entries" { + resetForTest(); + updateFromVirtioGpu(.{ + .vendor_id = 0x1AF4, + .device_id = 0x1050, + .pci_bus = 0, + .pci_device = 2, + .pci_function = 0, + .hardware_backed = true, + .connected = true, + .scanout_count = 2, + .active_scanout = 1, + .current_width = 1920, + .current_height = 1080, + .preferred_width = 1920, + .preferred_height = 1080, + .physical_width_mm = 520, + .physical_height_mm = 320, + .manufacturer_id = 0x1111, + .product_code = 0x2222, + .serial_number = 0x33334444, + .capability_flags = abi.display_capability_displayid_extension | abi.display_capability_preferred_timing, + .edid = &.{ 0x00, 0xFF, 0xFF, 0xFF }, + .scanouts = &.{ + .{ + .connected = false, + .scanout_index = 0, + .current_width = 0, + .current_height = 0, + .preferred_width = 0, + .preferred_height = 0, + .physical_width_mm = 0, + .physical_height_mm = 0, + .manufacturer_id = 0, + .product_code = 0, + .serial_number = 0, + .capability_flags = 0, + .edid_length = 0, + }, + .{ + .connected = true, + .scanout_index = 1, + .current_width = 1920, + .current_height = 1080, + .preferred_width = 1920, + .preferred_height = 1080, + .physical_width_mm = 520, + .physical_height_mm = 320, + .manufacturer_id = 0x1111, + .product_code = 0x2222, + .serial_number = 0x33334444, + .capability_flags = abi.display_capability_displayid_extension | abi.display_capability_preferred_timing, + .edid_length = 256, + }, + }, + }); + + try std.testing.expectEqual(@as(u16, 2), outputCount()); + const disconnected = outputEntry(0); + try std.testing.expectEqual(@as(u8, 0), disconnected.connected); + try std.testing.expectEqual(@as(u8, abi.display_connector_none), disconnected.connector_type); + const connected = outputEntry(1); + try std.testing.expectEqual(@as(u8, 1), connected.connected); + try std.testing.expectEqual(@as(u8, 1), connected.scanout_index); + try std.testing.expectEqual(@as(u8, abi.display_connector_displayport), connected.connector_type); + try std.testing.expectEqual(@as(u16, 1920), connected.current_width); +} diff --git a/src/baremetal/tool_exec.zig b/src/baremetal/tool_exec.zig index 0299829d..e608e10d 100644 --- a/src/baremetal/tool_exec.zig +++ b/src/baremetal/tool_exec.zig @@ -154,7 +154,7 @@ fn execute( if (depth > max_script_depth) return error.ScriptDepthExceeded; if (std.ascii.eqlIgnoreCase(parsed.name, "help")) { - try stdout_buffer.appendLine("OpenClaw bare-metal builtins: help, echo, cat, write-file, mkdir, stat, ls, package-info, package-verify, package-app, package-display, package-ls, package-cat, package-delete, package-release-list, package-release-info, package-release-save, package-release-activate, package-release-delete, package-release-prune, package-release-channel-list, package-release-channel-info, package-release-channel-set, package-release-channel-activate, app-list, app-info, app-state, app-history, app-stdout, app-stderr, app-trust, app-connector, app-plan-list, app-plan-info, app-plan-active, app-plan-save, app-plan-apply, app-plan-delete, app-suite-list, app-suite-info, app-suite-save, app-suite-apply, app-suite-run, app-suite-delete, app-suite-release-list, app-suite-release-info, app-suite-release-save, app-suite-release-activate, app-suite-release-delete, app-suite-release-prune, app-suite-release-channel-list, app-suite-release-channel-info, app-suite-release-channel-set, app-suite-release-channel-activate, app-delete, app-autorun-list, app-autorun-add, app-autorun-remove, app-autorun-run, workspace-plan-list, workspace-plan-info, workspace-plan-active, workspace-plan-save, workspace-plan-apply, workspace-plan-delete, workspace-plan-release-list, workspace-plan-release-info, workspace-plan-release-save, workspace-plan-release-activate, workspace-plan-release-delete, workspace-plan-release-prune, workspace-suite-list, workspace-suite-info, workspace-suite-save, workspace-suite-apply, workspace-suite-run, workspace-suite-delete, workspace-suite-release-list, workspace-suite-release-info, workspace-suite-release-save, workspace-suite-release-activate, workspace-suite-release-delete, workspace-suite-release-prune, workspace-suite-release-channel-list, workspace-suite-release-channel-info, workspace-suite-release-channel-set, workspace-suite-release-channel-activate, workspace-list, workspace-info, workspace-save, workspace-apply, workspace-run, workspace-state, workspace-history, workspace-stdout, workspace-stderr, workspace-delete, workspace-release-list, workspace-release-info, workspace-release-save, workspace-release-activate, workspace-release-delete, workspace-release-prune, workspace-release-channel-list, workspace-release-channel-info, workspace-release-channel-set, workspace-release-channel-activate, workspace-autorun-list, workspace-autorun-add, workspace-autorun-remove, workspace-autorun-run, trust-list, trust-info, trust-active, trust-select, trust-delete, runtime-snapshot, runtime-sessions, runtime-session, display-info, display-modes, display-set, run-script, run-package, app-run"); + try stdout_buffer.appendLine("OpenClaw bare-metal builtins: help, echo, cat, write-file, mkdir, stat, ls, package-info, package-verify, package-app, package-display, package-ls, package-cat, package-delete, package-release-list, package-release-info, package-release-save, package-release-activate, package-release-delete, package-release-prune, package-release-channel-list, package-release-channel-info, package-release-channel-set, package-release-channel-activate, app-list, app-info, app-state, app-history, app-stdout, app-stderr, app-trust, app-connector, app-plan-list, app-plan-info, app-plan-active, app-plan-save, app-plan-apply, app-plan-delete, app-suite-list, app-suite-info, app-suite-save, app-suite-apply, app-suite-run, app-suite-delete, app-suite-release-list, app-suite-release-info, app-suite-release-save, app-suite-release-activate, app-suite-release-delete, app-suite-release-prune, app-suite-release-channel-list, app-suite-release-channel-info, app-suite-release-channel-set, app-suite-release-channel-activate, app-delete, app-autorun-list, app-autorun-add, app-autorun-remove, app-autorun-run, workspace-plan-list, workspace-plan-info, workspace-plan-active, workspace-plan-save, workspace-plan-apply, workspace-plan-delete, workspace-plan-release-list, workspace-plan-release-info, workspace-plan-release-save, workspace-plan-release-activate, workspace-plan-release-delete, workspace-plan-release-prune, workspace-suite-list, workspace-suite-info, workspace-suite-save, workspace-suite-apply, workspace-suite-run, workspace-suite-delete, workspace-suite-release-list, workspace-suite-release-info, workspace-suite-release-save, workspace-suite-release-activate, workspace-suite-release-delete, workspace-suite-release-prune, workspace-suite-release-channel-list, workspace-suite-release-channel-info, workspace-suite-release-channel-set, workspace-suite-release-channel-activate, workspace-list, workspace-info, workspace-save, workspace-apply, workspace-run, workspace-state, workspace-history, workspace-stdout, workspace-stderr, workspace-delete, workspace-release-list, workspace-release-info, workspace-release-save, workspace-release-activate, workspace-release-delete, workspace-release-prune, workspace-release-channel-list, workspace-release-channel-info, workspace-release-channel-set, workspace-release-channel-activate, workspace-autorun-list, workspace-autorun-add, workspace-autorun-remove, workspace-autorun-run, trust-list, trust-info, trust-active, trust-select, trust-delete, runtime-snapshot, runtime-sessions, runtime-session, display-info, display-outputs, display-output, display-modes, display-set, run-script, run-package, app-run"); return; } @@ -2965,6 +2965,75 @@ fn execute( return; } + if (std.ascii.eqlIgnoreCase(parsed.name, "display-outputs")) { + if (parsed.rest.len != 0) { + exit_code.* = 2; + try stderr_buffer.appendLine("usage: display-outputs"); + return; + } + ensureDisplayReady(); + var index: u16 = 0; + while (index < display_output.outputCount()) : (index += 1) { + const entry = display_output.outputEntry(index); + try stdout_buffer.appendFmt( + "output {d} scanout={d} connector={s} connected={d} current={d}x{d} preferred={d}x{d} capabilities=0x{x}\n", + .{ + index, + entry.scanout_index, + displayConnectorName(entry.connector_type), + entry.connected, + entry.current_width, + entry.current_height, + entry.preferred_width, + entry.preferred_height, + entry.capability_flags, + }, + ); + } + return; + } + + if (std.ascii.eqlIgnoreCase(parsed.name, "display-output")) { + const index_arg = parseFirstArg(parsed.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "display-output "); + return; + }; + if (index_arg.rest.len != 0) { + exit_code.* = 2; + try stderr_buffer.appendLine("usage: display-output "); + return; + } + const index = std.fmt.parseInt(u16, index_arg.arg, 10) catch { + exit_code.* = 2; + try stderr_buffer.appendLine("usage: display-output "); + return; + }; + ensureDisplayReady(); + if (index >= display_output.outputCount()) { + exit_code.* = 1; + try stderr_buffer.appendLine("display-output failed: NotFound"); + return; + } + const entry = display_output.outputEntry(index); + try stdout_buffer.appendFmt( + "index={d} scanout={d} connector={s} connected={d} current={d}x{d} preferred={d}x{d} capabilities=0x{x} edid_present={d}\n", + .{ + index, + entry.scanout_index, + displayConnectorName(entry.connector_type), + entry.connected, + entry.current_width, + entry.current_height, + entry.preferred_width, + entry.preferred_height, + entry.capability_flags, + entry.edid_present, + }, + ); + return; + } + if (std.ascii.eqlIgnoreCase(parsed.name, "display-modes")) { if (parsed.rest.len != 0) { exit_code.* = 2; @@ -3710,6 +3779,16 @@ test "baremetal tool exec reports current display info and supported modes" { try std.testing.expect(std.mem.indexOf(u8, info_result.stdout, "controller=bochs-bga") != null); try std.testing.expect(std.mem.indexOf(u8, info_result.stdout, "current=640x400") != null); + var outputs_result = try runCapture(std.testing.allocator, "display-outputs", 256, 256); + defer outputs_result.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), outputs_result.exit_code); + try std.testing.expect(std.mem.indexOf(u8, outputs_result.stdout, "output 0 scanout=0 connector=virtual connected=0 current=640x400 preferred=640x400") != null); + + var output_result = try runCapture(std.testing.allocator, "display-output 0", 256, 256); + defer output_result.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), output_result.exit_code); + try std.testing.expect(std.mem.indexOf(u8, output_result.stdout, "index=0 scanout=0 connector=virtual connected=0 current=640x400 preferred=640x400") != null); + var modes_result = try runCapture(std.testing.allocator, "display-modes", 256, 256); defer modes_result.deinit(std.testing.allocator); try std.testing.expectEqual(@as(u8, 0), modes_result.exit_code); diff --git a/src/baremetal/tool_service.zig b/src/baremetal/tool_service.zig index 0a3904d1..01fb2651 100644 --- a/src/baremetal/tool_service.zig +++ b/src/baremetal/tool_service.zig @@ -304,6 +304,8 @@ fn handleFramedPayload( .app_autorun_remove => |package_name| try handleAppAutorunRemoveRequest(allocator, package_name, payload_limit), .app_autorun_run => try handleAppAutorunRunRequest(allocator, stdout_limit, stderr_limit, payload_limit), .display_info => try handleDisplayInfoRequest(allocator, payload_limit), + .display_outputs => try handleDisplayOutputsRequest(allocator, payload_limit), + .display_output => |output_index| try handleDisplayOutputRequest(allocator, output_index, payload_limit), .display_modes => try handleDisplayModesRequest(allocator, payload_limit), .display_set => |display_mode| try handleDisplaySetRequest(allocator, display_mode.width, display_mode.height, payload_limit), .trust_install => |trust_request| try handleTrustInstallRequest(allocator, trust_request.path, trust_request.body, payload_limit), @@ -1831,6 +1833,67 @@ fn handleDisplayInfoRequest(allocator: std.mem.Allocator, payload_limit: usize) return response; } +fn handleDisplayOutputsRequest(allocator: std.mem.Allocator, payload_limit: usize) Error![]u8 { + ensureDisplayReady(); + var out: std.ArrayList(u8) = .empty; + defer out.deinit(allocator); + + var index: u16 = 0; + while (index < display_output.outputCount()) : (index += 1) { + const entry = display_output.outputEntry(index); + const line = try std.fmt.allocPrint( + allocator, + "output {d} scanout={d} connector={s} connected={d} current={d}x{d} preferred={d}x{d} capabilities=0x{x}\n", + .{ + index, + entry.scanout_index, + displayConnectorName(entry.connector_type), + entry.connected, + entry.current_width, + entry.current_height, + entry.preferred_width, + entry.preferred_height, + entry.capability_flags, + }, + ); + defer allocator.free(line); + if (out.items.len + line.len > payload_limit) return error.ResponseTooLarge; + try out.appendSlice(allocator, line); + } + + return out.toOwnedSlice(allocator); +} + +fn handleDisplayOutputRequest(allocator: std.mem.Allocator, output_index_text: []const u8, payload_limit: usize) Error![]u8 { + ensureDisplayReady(); + const index = std.fmt.parseInt(u16, output_index_text, 10) catch { + return formatOperationError(allocator, "DISPLAYOUTPUT", error.InvalidFrame, payload_limit); + }; + if (index >= display_output.outputCount()) { + return formatOperationError(allocator, "DISPLAYOUTPUT", error.NotFound, payload_limit); + } + const entry = display_output.outputEntry(index); + const response = try std.fmt.allocPrint( + allocator, + "index={d} scanout={d} connector={s} connected={d} current={d}x{d} preferred={d}x{d} capabilities=0x{x} edid_present={d}\n", + .{ + index, + entry.scanout_index, + displayConnectorName(entry.connector_type), + entry.connected, + entry.current_width, + entry.current_height, + entry.preferred_width, + entry.preferred_height, + entry.capability_flags, + entry.edid_present, + }, + ); + errdefer allocator.free(response); + if (response.len > payload_limit) return error.ResponseTooLarge; + return response; +} + fn handleDisplayModesRequest(allocator: std.mem.Allocator, payload_limit: usize) Error![]u8 { ensureDisplayReady(); var out: std.ArrayList(u8) = .empty; @@ -2238,13 +2301,25 @@ test "baremetal tool service parses typed framed requests" { else => return error.InvalidFrame, } - const display_modes = try parseFramedRequest("REQ 27 DISPLAYMODES"); + const display_outputs = try parseFramedRequest("REQ 27 DISPLAYOUTPUTS"); + switch (display_outputs.operation) { + .display_outputs => {}, + else => return error.InvalidFrame, + } + + const display_output_request = try parseFramedRequest("REQ 28 DISPLAYOUTPUT 0"); + switch (display_output_request.operation) { + .display_output => |payload| try std.testing.expectEqualStrings("0", payload), + else => return error.InvalidFrame, + } + + const display_modes = try parseFramedRequest("REQ 29 DISPLAYMODES"); switch (display_modes.operation) { .display_modes => {}, else => return error.InvalidFrame, } - const display_set = try parseFramedRequest("REQ 28 DISPLAYSET 800 600"); + const display_set = try parseFramedRequest("REQ 30 DISPLAYSET 800 600"); switch (display_set.operation) { .display_set => |payload| { try std.testing.expectEqual(@as(u16, 800), payload.width); @@ -3230,17 +3305,27 @@ test "baremetal tool service reports display info and supported modes" { try std.testing.expect(std.mem.indexOf(u8, info_response, "connector=virtual") != null); try std.testing.expect(std.mem.indexOf(u8, info_response, "current=640x400") != null); - const modes_response = try handleFramedRequest(std.testing.allocator, "REQ 62 DISPLAYMODES", 512, 256, 512); + const outputs_response = try handleFramedRequest(std.testing.allocator, "REQ 62 DISPLAYOUTPUTS", 512, 256, 512); + defer std.testing.allocator.free(outputs_response); + try std.testing.expect(std.mem.startsWith(u8, outputs_response, "RESP 62 ")); + try std.testing.expect(std.mem.indexOf(u8, outputs_response, "output 0 scanout=0 connector=virtual connected=0 current=640x400 preferred=640x400") != null); + + const output_response = try handleFramedRequest(std.testing.allocator, "REQ 63 DISPLAYOUTPUT 0", 512, 256, 512); + defer std.testing.allocator.free(output_response); + try std.testing.expect(std.mem.startsWith(u8, output_response, "RESP 63 ")); + try std.testing.expect(std.mem.indexOf(u8, output_response, "index=0 scanout=0 connector=virtual connected=0 current=640x400 preferred=640x400") != null); + + const modes_response = try handleFramedRequest(std.testing.allocator, "REQ 64 DISPLAYMODES", 512, 256, 512); defer std.testing.allocator.free(modes_response); - try std.testing.expect(std.mem.startsWith(u8, modes_response, "RESP 62 ")); + try std.testing.expect(std.mem.startsWith(u8, modes_response, "RESP 64 ")); try std.testing.expect(std.mem.indexOf(u8, modes_response, "mode 0 640x400") != null); try std.testing.expect(std.mem.indexOf(u8, modes_response, "mode 4 1280x1024") != null); - const set_response = try handleFramedRequest(std.testing.allocator, "REQ 63 DISPLAYSET 800 600", 512, 256, 512); + const set_response = try handleFramedRequest(std.testing.allocator, "REQ 65 DISPLAYSET 800 600", 512, 256, 512); defer std.testing.allocator.free(set_response); - try std.testing.expectEqualStrings("RESP 63 16\nDISPLAY 800x600\n", set_response); + try std.testing.expectEqualStrings("RESP 65 16\nDISPLAY 800x600\n", set_response); - const updated_info = try handleFramedRequest(std.testing.allocator, "REQ 64 DISPLAYINFO", 512, 256, 512); + const updated_info = try handleFramedRequest(std.testing.allocator, "REQ 66 DISPLAYINFO", 512, 256, 512); defer std.testing.allocator.free(updated_info); try std.testing.expect(std.mem.indexOf(u8, updated_info, "current=800x600") != null); } diff --git a/src/baremetal/tool_service/codec.zig b/src/baremetal/tool_service/codec.zig index 936553d3..d15163ac 100644 --- a/src/baremetal/tool_service/codec.zig +++ b/src/baremetal/tool_service/codec.zig @@ -132,6 +132,8 @@ pub const RequestOp = enum { app_autorun_remove, app_autorun_run, display_info, + display_outputs, + display_output, display_modes, display_set, trust_install, @@ -430,6 +432,8 @@ pub const FramedRequest = struct { app_autorun_remove: []const u8, app_autorun_run: void, display_info: void, + display_outputs: void, + display_output: []const u8, display_modes: void, display_set: DisplayModeRequest, trust_install: PutRequest, @@ -2653,6 +2657,35 @@ pub fn parseFramedRequestPrefix(request: []const u8) Error!ConsumedRequest { }; } + if (std.ascii.eqlIgnoreCase(op_part.token, "DISPLAYOUTPUTS")) { + if (op_part.rest.len != 0) return error.InvalidFrame; + if (newline_index != null) { + return .{ + .framed = .{ .request_id = request_id, .operation = .{ .display_outputs = {} } }, + .consumed_len = prefix_len + newline_index.? + 1, + }; + } + return .{ + .framed = .{ .request_id = request_id, .operation = .{ .display_outputs = {} } }, + .consumed_len = request.len, + }; + } + + if (std.ascii.eqlIgnoreCase(op_part.token, "DISPLAYOUTPUT")) { + const output_index_part = try splitFirstToken(op_part.rest); + if (output_index_part.rest.len != 0) return error.InvalidFrame; + if (newline_index != null) { + return .{ + .framed = .{ .request_id = request_id, .operation = .{ .display_output = output_index_part.token } }, + .consumed_len = prefix_len + newline_index.? + 1, + }; + } + return .{ + .framed = .{ .request_id = request_id, .operation = .{ .display_output = output_index_part.token } }, + .consumed_len = request.len, + }; + } + if (std.ascii.eqlIgnoreCase(op_part.token, "DISPLAYMODES")) { if (op_part.rest.len != 0) return error.InvalidFrame; if (newline_index != null) { diff --git a/src/baremetal/virtio_gpu.zig b/src/baremetal/virtio_gpu.zig index 3ec68ae8..e3f45e7a 100644 --- a/src/baremetal/virtio_gpu.zig +++ b/src/baremetal/virtio_gpu.zig @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-only const std = @import("std"); const builtin = @import("builtin"); +const abi = @import("abi.zig"); const pci = @import("pci.zig"); const edid = @import("edid.zig"); const display_output = @import("display_output.zig"); @@ -224,6 +225,12 @@ pub const ProbeResult = struct { edid_length: u16, }; +const DetailedProbe = struct { + result: ProbeResult, + scanouts: [max_scanouts]display_output.VirtioGpuScanoutUpdate, + scanout_count: u8, +}; + pub const PresentStats = struct { resource_create_count: u32 = 0, resource_unref_count: u32 = 0, @@ -585,56 +592,120 @@ pub fn scanoutPixel(x: u16, y: u16) u32 { return scanout_pixels[(@as(usize, y) * max_present_width) + @as(usize, x)]; } -fn selectConnectedScanout(response: RespDisplayInfo, scanout_limit: u32) ?struct { index: u8, width: u16, height: u16 } { +fn enumerateScanouts(device: pci.VirtioGpuDevice, queue_notify_off: u16, display_info: RespDisplayInfo, scanout_limit: u32) ProbeError!struct { + scanouts: [max_scanouts]display_output.VirtioGpuScanoutUpdate, + scanout_count: u8, +} { + var scanouts: [max_scanouts]display_output.VirtioGpuScanoutUpdate = undefined; + @memset(&scanouts, std.mem.zeroes(display_output.VirtioGpuScanoutUpdate)); + + const limit: usize = @min(@as(usize, scanout_limit), max_scanouts); var idx: usize = 0; - while (idx < @min(@as(usize, scanout_limit), max_scanouts)) : (idx += 1) { - const mode = response.pmodes[idx]; - if (mode.enabled == 0) continue; - return .{ - .index = @intCast(idx), - .width = @intCast(@min(mode.rect.width, std.math.maxInt(u16))), - .height = @intCast(@min(mode.rect.height, std.math.maxInt(u16))), + while (idx < limit) : (idx += 1) { + const mode = display_info.pmodes[idx]; + if (mode.enabled == 0) { + scanouts[idx] = .{ + .connected = false, + .scanout_index = @intCast(idx), + .current_width = 0, + .current_height = 0, + .preferred_width = 0, + .preferred_height = 0, + .physical_width_mm = 0, + .physical_height_mm = 0, + .manufacturer_id = 0, + .product_code = 0, + .serial_number = 0, + .capability_flags = 0, + .edid_length = 0, + }; + continue; + } + + const edid_response = try sendGetEdid(device, queue_notify_off, @intCast(idx)); + const edid_size = @min(edid_response.size, @as(u32, display_output.max_edid_bytes)); + if (edid_size < edid.block_len) return error.InvalidEdidResponse; + const parsed = edid.parse(edid_response.edid[0..edid_size]) catch return error.InvalidEdid; + scanouts[idx] = .{ + .connected = true, + .scanout_index = @intCast(idx), + .current_width = @intCast(@min(mode.rect.width, std.math.maxInt(u16))), + .current_height = @intCast(@min(mode.rect.height, std.math.maxInt(u16))), + .preferred_width = if (parsed.preferred_timing) |timing| timing.h_active else @intCast(@min(mode.rect.width, std.math.maxInt(u16))), + .preferred_height = if (parsed.preferred_timing) |timing| timing.v_active else @intCast(@min(mode.rect.height, std.math.maxInt(u16))), + .physical_width_mm = parsed.physical_width_mm, + .physical_height_mm = parsed.physical_height_mm, + .manufacturer_id = parsed.manufacturer_id, + .product_code = parsed.product_code, + .serial_number = parsed.serial_number, + .capability_flags = parsed.capability_flags, + .edid_length = @intCast(edid_size), }; } + + return .{ + .scanouts = scanouts, + .scanout_count = @intCast(limit), + }; +} + +fn selectScanoutForConnector( + scanouts: []const display_output.VirtioGpuScanoutUpdate, + preferred_connector: ?u8, +) ?display_output.VirtioGpuScanoutUpdate { + if (preferred_connector) |connector| { + for (scanouts) |scanout| { + if (!scanout.connected) continue; + if (display_output.inferConnectorType(scanout.capability_flags) == connector) return scanout; + } + } + for (scanouts) |scanout| { + if (scanout.connected) return scanout; + } return null; } -pub fn probeDisplay() ProbeError!ProbeResult { +fn probeDisplayDetailed(preferred_connector: ?u8) ProbeError!DetailedProbe { if (!hardwareBacked()) return error.UnsupportedPlatform; const device = pci.discoverVirtioGpuDevice() orelse return error.DeviceNotFound; const queue_notify_off = try initTransport(device); const scanout_limit = @min(deviceCfg(device).num_scanouts, @as(u32, max_scanouts)); const display_info = try sendGetDisplayInfo(device, queue_notify_off); - const active = selectConnectedScanout(display_info, scanout_limit) orelse return error.NoConnectedScanout; - const edid_response = try sendGetEdid(device, queue_notify_off, active.index); - const edid_size = @min(edid_response.size, @as(u32, display_output.max_edid_bytes)); - if (edid_size < edid.block_len) return error.InvalidEdidResponse; - - const parsed = edid.parse(edid_response.edid[0..edid_size]) catch return error.InvalidEdid; + const enumerated = try enumerateScanouts(device, queue_notify_off, display_info, scanout_limit); + const active = selectScanoutForConnector(enumerated.scanouts[0..enumerated.scanout_count], preferred_connector) orelse return error.NoConnectedScanout; return .{ - .vendor_id = device.vendor_id, - .device_id = device.device_id, - .pci_bus = device.location.bus, - .pci_device = device.location.device, - .pci_function = device.location.function, - .scanout_count = @intCast(scanout_limit), - .active_scanout = active.index, - .current_width = active.width, - .current_height = active.height, - .preferred_width = if (parsed.preferred_timing) |timing| timing.h_active else active.width, - .preferred_height = if (parsed.preferred_timing) |timing| timing.v_active else active.height, - .physical_width_mm = parsed.physical_width_mm, - .physical_height_mm = parsed.physical_height_mm, - .manufacturer_id = parsed.manufacturer_id, - .product_code = parsed.product_code, - .serial_number = parsed.serial_number, - .capability_flags = parsed.capability_flags, - .edid_length = @intCast(edid_size), + .result = .{ + .vendor_id = device.vendor_id, + .device_id = device.device_id, + .pci_bus = device.location.bus, + .pci_device = device.location.device, + .pci_function = device.location.function, + .scanout_count = enumerated.scanout_count, + .active_scanout = active.scanout_index, + .current_width = active.current_width, + .current_height = active.current_height, + .preferred_width = active.preferred_width, + .preferred_height = active.preferred_height, + .physical_width_mm = active.physical_width_mm, + .physical_height_mm = active.physical_height_mm, + .manufacturer_id = active.manufacturer_id, + .product_code = active.product_code, + .serial_number = active.serial_number, + .capability_flags = active.capability_flags, + .edid_length = active.edid_length, + }, + .scanouts = enumerated.scanouts, + .scanout_count = enumerated.scanout_count, }; } +pub fn probeDisplay() ProbeError!ProbeResult { + return (try probeDisplayDetailed(null)).result; +} + pub fn probeAndPublish() ProbeError!ProbeResult { - const result = try probeDisplay(); + const detailed = try probeDisplayDetailed(null); + const result = detailed.result; const device = pci.discoverVirtioGpuDevice() orelse return error.DeviceNotFound; const queue_notify_off = try initTransport(device); const edid_response = try sendGetEdid(device, queue_notify_off, result.active_scanout); @@ -660,55 +731,37 @@ pub fn probeAndPublish() ProbeError!ProbeResult { .serial_number = result.serial_number, .capability_flags = result.capability_flags, .edid = edid_response.edid[0..edid_len], + .scanouts = detailed.scanouts[0..detailed.scanout_count], }); return result; } pub fn probeAndPresentPattern() ProbeError!ProbeResult { + return probeAndPresentPatternForConnector(null); +} + +pub fn probeAndPresentPatternForConnector(preferred_connector: ?u8) ProbeError!ProbeResult { if (!hardwareBacked()) return error.UnsupportedPlatform; last_present_stats = .{}; @memset(&scanout_pixels, 0); + const detailed = try probeDisplayDetailed(preferred_connector); + const result = detailed.result; + if (result.current_width > max_present_width or result.current_height > max_present_height) return error.FramebufferTooLarge; + const device = pci.discoverVirtioGpuDevice() orelse return error.DeviceNotFound; const queue_notify_off = try initTransport(device); - const scanout_limit = @min(deviceCfg(device).num_scanouts, @as(u32, max_scanouts)); - const display_info = try sendGetDisplayInfo(device, queue_notify_off); - const active = selectConnectedScanout(display_info, scanout_limit) orelse return error.NoConnectedScanout; - if (active.width > max_present_width or active.height > max_present_height) return error.FramebufferTooLarge; - - const edid_response = try sendGetEdid(device, queue_notify_off, active.index); + const edid_response = try sendGetEdid(device, queue_notify_off, result.active_scanout); const edid_size = @min(edid_response.size, @as(u32, display_output.max_edid_bytes)); if (edid_size < edid.block_len) return error.InvalidEdidResponse; - const parsed = edid.parse(edid_response.edid[0..edid_size]) catch return error.InvalidEdid; - fillProbePattern(active.width, active.height); - try sendResourceCreate2d(device, queue_notify_off, active.width, active.height); + fillProbePattern(result.current_width, result.current_height); + try sendResourceCreate2d(device, queue_notify_off, result.current_width, result.current_height); errdefer sendResourceUnref(device, queue_notify_off) catch {}; - try sendResourceAttachBacking(device, queue_notify_off, active.width, active.height); - try sendSetScanout(device, queue_notify_off, active.index, active.width, active.height); - try sendTransferToHost2d(device, queue_notify_off, active.width, active.height); - try sendResourceFlush(device, queue_notify_off, active.width, active.height); - - const result: ProbeResult = .{ - .vendor_id = device.vendor_id, - .device_id = device.device_id, - .pci_bus = device.location.bus, - .pci_device = device.location.device, - .pci_function = device.location.function, - .scanout_count = @intCast(scanout_limit), - .active_scanout = active.index, - .current_width = active.width, - .current_height = active.height, - .preferred_width = if (parsed.preferred_timing) |timing| timing.h_active else active.width, - .preferred_height = if (parsed.preferred_timing) |timing| timing.v_active else active.height, - .physical_width_mm = parsed.physical_width_mm, - .physical_height_mm = parsed.physical_height_mm, - .manufacturer_id = parsed.manufacturer_id, - .product_code = parsed.product_code, - .serial_number = parsed.serial_number, - .capability_flags = parsed.capability_flags, - .edid_length = @intCast(edid_size), - }; + try sendResourceAttachBacking(device, queue_notify_off, result.current_width, result.current_height); + try sendSetScanout(device, queue_notify_off, result.active_scanout, result.current_width, result.current_height); + try sendTransferToHost2d(device, queue_notify_off, result.current_width, result.current_height); + try sendResourceFlush(device, queue_notify_off, result.current_width, result.current_height); display_output.updateFromVirtioGpu(.{ .vendor_id = result.vendor_id, .device_id = result.device_id, @@ -730,21 +783,88 @@ pub fn probeAndPresentPattern() ProbeError!ProbeResult { .serial_number = result.serial_number, .capability_flags = result.capability_flags, .edid = edid_response.edid[0..edid_size], + .scanouts = detailed.scanouts[0..detailed.scanout_count], }); return result; } test "virtio gpu scanout selector chooses first enabled output" { - var response = std.mem.zeroes(RespDisplayInfo); - response.pmodes[0].enabled = 0; - response.pmodes[1].enabled = 1; - response.pmodes[1].rect.width = 1280; - response.pmodes[1].rect.height = 720; - - const selected = selectConnectedScanout(response, 2) orelse return error.TestUnexpectedResult; - try std.testing.expectEqual(@as(u8, 1), selected.index); - try std.testing.expectEqual(@as(u16, 1280), selected.width); - try std.testing.expectEqual(@as(u16, 720), selected.height); + const scanouts = [_]display_output.VirtioGpuScanoutUpdate{ + .{ + .connected = false, + .scanout_index = 0, + .current_width = 0, + .current_height = 0, + .preferred_width = 0, + .preferred_height = 0, + .physical_width_mm = 0, + .physical_height_mm = 0, + .manufacturer_id = 0, + .product_code = 0, + .serial_number = 0, + .capability_flags = 0, + .edid_length = 0, + }, + .{ + .connected = true, + .scanout_index = 1, + .current_width = 1280, + .current_height = 720, + .preferred_width = 1280, + .preferred_height = 720, + .physical_width_mm = 0, + .physical_height_mm = 0, + .manufacturer_id = 0, + .product_code = 0, + .serial_number = 0, + .capability_flags = 0, + .edid_length = 0, + }, + }; + + const selected = selectScanoutForConnector(&scanouts, null) orelse return error.TestUnexpectedResult; + try std.testing.expectEqual(@as(u8, 1), selected.scanout_index); + try std.testing.expectEqual(@as(u16, 1280), selected.current_width); + try std.testing.expectEqual(@as(u16, 720), selected.current_height); +} + +test "virtio gpu scanout selector prefers requested connector when present" { + const scanouts = [_]display_output.VirtioGpuScanoutUpdate{ + .{ + .connected = true, + .scanout_index = 0, + .current_width = 1280, + .current_height = 720, + .preferred_width = 1280, + .preferred_height = 720, + .physical_width_mm = 0, + .physical_height_mm = 0, + .manufacturer_id = 0, + .product_code = 0, + .serial_number = 0, + .capability_flags = abi.display_capability_hdmi_vendor_data, + .edid_length = 128, + }, + .{ + .connected = true, + .scanout_index = 1, + .current_width = 1920, + .current_height = 1080, + .preferred_width = 1920, + .preferred_height = 1080, + .physical_width_mm = 0, + .physical_height_mm = 0, + .manufacturer_id = 0, + .product_code = 0, + .serial_number = 0, + .capability_flags = abi.display_capability_displayid_extension, + .edid_length = 128, + }, + }; + + const selected = selectScanoutForConnector(&scanouts, abi.display_connector_displayport) orelse return error.TestUnexpectedResult; + try std.testing.expectEqual(@as(u8, 1), selected.scanout_index); + try std.testing.expectEqual(@as(u16, 1920), selected.current_width); } test "virtio gpu probe pattern paints scanout pixels" { diff --git a/src/baremetal_main.zig b/src/baremetal_main.zig index d8b3f359..49f5da9f 100644 --- a/src/baremetal_main.zig +++ b/src/baremetal_main.zig @@ -655,6 +655,8 @@ const VirtioGpuDisplayProbeError = error{ EdidLengthMismatch, EdidHeaderMismatch, CapabilityFlagsMismatch, + OutputEntryCountMismatch, + OutputEntryMismatch, PresentStatsMismatch, RenderedPixelMismatch, }; @@ -958,6 +960,14 @@ pub export fn oc_display_output_edid_byte(index: u16) u8 { return display_output.edidByte(index); } +pub export fn oc_display_output_entry_count() u16 { + return display_output.outputCount(); +} + +pub export fn oc_display_output_entry(index: u16) abi.BaremetalDisplayOutputEntry { + return display_output.outputEntry(index); +} + pub export fn oc_keyboard_state_ptr() *const BaremetalKeyboardState { return ps2_input.keyboardStatePtr(); } @@ -3408,6 +3418,8 @@ fn runRtl8139TcpProbe() Rtl8139TcpProbeError!void { const batch_request_id_package_asset_get: u32 = 59; const batch_request_id_display_info: u32 = 60; const batch_request_id_display_modes: u32 = 61; + const batch_request_id_display_outputs: u32 = 160; + const batch_request_id_display_output: u32 = 161; const batch_request_id_app_connector: u32 = 62; const batch_request_id_app_trust: u32 = 63; const batch_request_id_app_info: u32 = 64; @@ -5729,7 +5741,7 @@ fn runRtl8139TcpProbe() Rtl8139TcpProbeError!void { client_b.local_window = 48; service_fba = std.heap.FixedBufferAllocator.init(&scratch.service_scratch); - const batch_service_request = std.fmt.bufPrint(&scratch.service_batch_request_buffer, "REQ {d} GET {s}\nREQ {d} PKGLS {s}\nREQ {d} PKGGET {s} {s}\nREQ {d} DISPLAYINFO\nREQ {d} DISPLAYMODES", .{ + const batch_service_request = std.fmt.bufPrint(&scratch.service_batch_request_buffer, "REQ {d} GET {s}\nREQ {d} PKGLS {s}\nREQ {d} PKGGET {s} {s}\nREQ {d} DISPLAYINFO\nREQ {d} DISPLAYOUTPUTS\nREQ {d} DISPLAYOUTPUT 0\nREQ {d} DISPLAYMODES", .{ batch_request_id_script, service_script_path, batch_request_id_package_asset_list, @@ -5738,6 +5750,8 @@ fn runRtl8139TcpProbe() Rtl8139TcpProbeError!void { package_name, package_asset_relative_path, batch_request_id_display_info, + batch_request_id_display_outputs, + batch_request_id_display_output, batch_request_id_display_modes, }) catch return error.ToolServiceFailed; const batch_service_request_payload = client_b.buildPayload(batch_service_request) catch |err| return mapTcpSessionProbeError(err); @@ -5752,7 +5766,7 @@ fn runRtl8139TcpProbe() Rtl8139TcpProbeError!void { scratch.packet_storage.payload[0..scratch.packet_storage.payload_len], 512, 256, - 512, + 1024, ) catch return error.ToolServiceFailed; const framebuffer_state = oc_framebuffer_state_ptr(); if (framebuffer_state.magic != abi.framebuffer_magic or oc_display_output_state_ptr().backend == abi.display_backend_none) { @@ -5778,6 +5792,32 @@ fn runRtl8139TcpProbe() Rtl8139TcpProbeError!void { display_state.capability_flags, }, ) catch return error.ToolServiceFailed; + var display_outputs_payload_buffer: [160]u8 = undefined; + const display_outputs_payload_expected = std.fmt.bufPrint( + &display_outputs_payload_buffer, + "output 0 scanout=0 connector=virtual connected={d} current={d}x{d} preferred={d}x{d} capabilities=0x{x}\n", + .{ + display_state.connected, + display_state.current_width, + display_state.current_height, + display_state.preferred_width, + display_state.preferred_height, + display_state.capability_flags, + }, + ) catch return error.ToolServiceFailed; + var display_output_payload_buffer: [176]u8 = undefined; + const display_output_payload_expected = std.fmt.bufPrint( + &display_output_payload_buffer, + "index=0 scanout=0 connector=virtual connected={d} current={d}x{d} preferred={d}x{d} capabilities=0x{x} edid_present=0\n", + .{ + display_state.connected, + display_state.current_width, + display_state.current_height, + display_state.preferred_width, + display_state.preferred_height, + display_state.capability_flags, + }, + ) catch return error.ToolServiceFailed; var display_modes_payload_buffer: [256]u8 = undefined; var display_modes_payload_len: usize = 0; var display_mode_index: u16 = 0; @@ -5794,7 +5834,7 @@ fn runRtl8139TcpProbe() Rtl8139TcpProbeError!void { display_modes_payload_len += line.len; } const display_modes_payload_expected = display_modes_payload_buffer[0..display_modes_payload_len]; - const batch_service_response_expected = std.fmt.bufPrint(&scratch.service_batch_response_expected_buffer, "RESP {d} {d}\n{s}RESP {d} 11\ndir config\nRESP {d} {d}\n{s}RESP {d} {d}\n{s}RESP {d} {d}\n{s}", .{ + const batch_service_response_expected = std.fmt.bufPrint(&scratch.service_batch_response_expected_buffer, "RESP {d} {d}\n{s}RESP {d} 11\ndir config\nRESP {d} {d}\n{s}RESP {d} {d}\n{s}RESP {d} {d}\n{s}RESP {d} {d}\n{s}RESP {d} {d}\n{s}", .{ batch_request_id_script, service_script_body.len, service_script_body, @@ -5805,6 +5845,12 @@ fn runRtl8139TcpProbe() Rtl8139TcpProbeError!void { batch_request_id_display_info, display_info_payload_expected.len, display_info_payload_expected, + batch_request_id_display_outputs, + display_outputs_payload_expected.len, + display_outputs_payload_expected, + batch_request_id_display_output, + display_output_payload_expected.len, + display_output_payload_expected, batch_request_id_display_modes, display_modes_payload_expected.len, display_modes_payload_expected, @@ -10367,8 +10413,10 @@ fn virtioGpuDisplayProbeFailureCode(err: VirtioGpuDisplayProbeError) u8 { error.EdidLengthMismatch => 0x61, error.EdidHeaderMismatch => 0x62, error.CapabilityFlagsMismatch => 0x63, - error.PresentStatsMismatch => 0x64, - error.RenderedPixelMismatch => 0x65, + error.OutputEntryCountMismatch => 0x64, + error.OutputEntryMismatch => 0x65, + error.PresentStatsMismatch => 0x66, + error.RenderedPixelMismatch => 0x67, }; } @@ -10383,12 +10431,12 @@ fn runToolExecProbe() ToolExecProbeError!void { resetBaremetalRuntimeForTest(); vga_text_console.clear(); - var scratch: [4096]u8 = undefined; + var scratch: [8192]u8 = undefined; var fba = std.heap.FixedBufferAllocator.init(&scratch); const allocator = fba.allocator(); const io: std.Io = undefined; - var help = pal_proc.runCaptureFreestanding(allocator, io, &.{"help"}, 1000, 4096, 128) catch |err| switch (err) { + var help = pal_proc.runCaptureFreestanding(allocator, io, &.{"help"}, 1000, 8192, 256) catch |err| switch (err) { error.OutOfMemory => return error.AllocatorExhausted, else => return error.HelpRunFailed, }; @@ -10567,7 +10615,7 @@ fn runToolRuntimeProbe() ToolRuntimeProbeError!void { fn runVirtioGpuDisplayProbe() VirtioGpuDisplayProbeError!void { resetBaremetalRuntimeForTest(); - const result = virtio_gpu.probeAndPresentPattern() catch |err| switch (err) { + const result = virtio_gpu.probeAndPresentPatternForConnector(abi.display_connector_virtual) catch |err| switch (err) { error.UnsupportedPlatform => return error.UnsupportedPlatform, error.DeviceNotFound => return error.DeviceNotFound, error.MissingCapabilities => return error.MissingCapabilities, @@ -10608,6 +10656,21 @@ fn runVirtioGpuDisplayProbe() VirtioGpuDisplayProbeError!void { if (output.capability_flags != result.capability_flags) return error.CapabilityFlagsMismatch; if ((output.capability_flags & abi.display_capability_digital_input) == 0) return error.CapabilityFlagsMismatch; if ((output.capability_flags & abi.display_capability_preferred_timing) == 0) return error.CapabilityFlagsMismatch; + if (oc_display_output_entry_count() != result.scanout_count) return error.OutputEntryCountMismatch; + if (result.active_scanout >= oc_display_output_entry_count()) return error.OutputEntryCountMismatch; + const active_entry = oc_display_output_entry(result.active_scanout); + if (active_entry.connected != 1 or + active_entry.scanout_index != result.active_scanout or + active_entry.connector_type != expected_connector or + active_entry.current_width != result.current_width or + active_entry.current_height != result.current_height or + active_entry.preferred_width != result.preferred_width or + active_entry.preferred_height != result.preferred_height or + active_entry.capability_flags != result.capability_flags or + active_entry.edid_present != 1) + { + return error.OutputEntryMismatch; + } if (present_stats.resource_create_count == 0 or present_stats.attach_backing_count == 0 or present_stats.scanout_set_count == 0 or @@ -18821,6 +18884,12 @@ test "baremetal display output export surface mirrors bounded bga framebuffer st try std.testing.expectEqual(@as(u16, 0), output.edid_length); try std.testing.expectEqual(@as(u16, 0), output.capability_flags); try std.testing.expectEqual(@as(u8, 0), oc_display_output_edid_byte(0)); + try std.testing.expectEqual(@as(u16, 1), oc_display_output_entry_count()); + const entry = oc_display_output_entry(0); + try std.testing.expectEqual(@as(u8, 0), entry.connected); + try std.testing.expectEqual(@as(u8, abi.display_connector_virtual), entry.connector_type); + try std.testing.expectEqual(@as(u16, 640), entry.current_width); + try std.testing.expectEqual(@as(u8, 0), entry.edid_present); try std.testing.expectEqual(abi.result_ok, oc_framebuffer_set_mode(1280, 720)); output = oc_display_output_state_ptr(); @@ -18829,6 +18898,9 @@ test "baremetal display output export surface mirrors bounded bga framebuffer st try std.testing.expectEqual(@as(u16, 1280), output.preferred_width); try std.testing.expectEqual(@as(u16, 720), output.preferred_height); try std.testing.expectEqual(@as(u8, 0), oc_display_output_edid_byte(127)); + const updated_entry = oc_display_output_entry(0); + try std.testing.expectEqual(@as(u16, 1280), updated_entry.current_width); + try std.testing.expectEqual(@as(u16, 720), updated_entry.current_height); } test "baremetal ethernet export surface initializes mock rtl8139 and loops a frame" { diff --git a/src/pal/framebuffer.zig b/src/pal/framebuffer.zig index 6254c142..455b45d4 100644 --- a/src/pal/framebuffer.zig +++ b/src/pal/framebuffer.zig @@ -5,6 +5,7 @@ const framebuffer_console = @import("../baremetal/framebuffer_console.zig"); pub const State = abi.BaremetalFramebufferState; pub const DisplayOutputState = abi.BaremetalDisplayOutputState; +pub const DisplayOutputEntry = abi.BaremetalDisplayOutputEntry; pub fn init() bool { return framebuffer_console.init(); @@ -34,6 +35,14 @@ pub fn displayOutputStatePtr() *const DisplayOutputState { return display_output.statePtr(); } +pub fn displayOutputCount() u16 { + return display_output.outputCount(); +} + +pub fn displayOutputEntry(index: u16) DisplayOutputEntry { + return display_output.outputEntry(index); +} + pub fn displayOutputEdidByte(index: u16) u8 { return display_output.edidByte(index); }