‹ projects

reverb-m1

Native wired VR on Apple silicon: Reverb G1 display bring-up and stereo demo
git clone https://umhau.org/code/reverb-m1.git
Log | Files | Refs

commit c265ec7a54fac4ebb3b87718c6fb9cd535c868e0
parent bd4f569e3845be6bb2b7f208cea8dd3c28d4cdad
Author: umhau <umhau@example.invalid>
Date:   Wed, 21 Oct 2026 12:00:01 +0000

Stereo demo scene (floor grid, depth markers), snapshot size/yaw options; hpmctl port-controller reader

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Diffstat:
M.gitignore | 1+
MSources/VRWorkstation/Renderer.swift | 46++++++++++++++--------------------------------
ASources/VRWorkstation/Scene.swift | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MSources/VRWorkstation/main.swift | 19++++++++++++++++---
Atools/hpmctl.c | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 177 insertions(+), 35 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -6,6 +6,7 @@ tools/hmdwake tools/hidcmd tools/displaywatch tools/testpattern +tools/hpmctl # Generated artifacts can contain private device identifiers or local paths. __pycache__/ diff --git a/Sources/VRWorkstation/Renderer.swift b/Sources/VRWorkstation/Renderer.swift @@ -12,8 +12,8 @@ final class Renderer: NSObject, MTKViewDelegate { let queue: MTLCommandQueue let pipeline: MTLRenderPipelineState let depth: MTLDepthStencilState - let cube: MTLBuffer - let cubeCount: Int + let sceneBuffer: MTLBuffer + let items: [SceneItem] let start = CACurrentMediaTime() /// Head orientation (world-from-head). Identity when head tracking is off. @@ -37,29 +37,9 @@ final class Renderer: NSObject, MTKViewDelegate { dd.depthCompareFunction = .less dd.isDepthWriteEnabled = true depth = dev.makeDepthStencilState(descriptor: dd)! - let verts = Renderer.makeCube() - cubeCount = verts.count - cube = dev.makeBuffer(bytes: verts, length: MemoryLayout<Vertex>.stride * verts.count)! - } - - static func makeCube() -> [Vertex] { - // 6 faces, each a distinct color, 2 triangles per face. - let faces: [(n: SIMD3<Float>, u: SIMD3<Float>, v: SIMD3<Float>, c: SIMD3<Float>)] = [ - ([1, 0, 0], [0, 0, -1], [0, 1, 0], [0.95, 0.30, 0.25]), - ([-1, 0, 0], [0, 0, 1], [0, 1, 0], [0.25, 0.80, 0.95]), - ([0, 1, 0], [1, 0, 0], [0, 0, -1], [0.40, 0.90, 0.35]), - ([0, -1, 0], [1, 0, 0], [0, 0, 1], [0.95, 0.85, 0.30]), - ([0, 0, 1], [1, 0, 0], [0, 1, 0], [0.75, 0.40, 0.95]), - ([0, 0, -1], [-1, 0, 0], [0, 1, 0], [0.95, 0.60, 0.20]), - ] - var out: [Vertex] = [] - for f in faces { - let c: SIMD3<Float> = f.n * 0.5 - let du: SIMD3<Float> = f.u * 0.5, dv: SIMD3<Float> = f.v * 0.5 - let p: [SIMD3<Float>] = [c - du - dv, c + du - dv, c + du + dv, c - du + dv] - for i in [0, 1, 2, 0, 2, 3] { out.append(Vertex(pos: p[i], normal: f.n, color: f.c)) } - } - return out + let scene = Scene.make() + items = scene.items + sceneBuffer = dev.makeBuffer(bytes: scene.vertices, length: MemoryLayout<Vertex>.stride * scene.vertices.count)! } func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} @@ -83,12 +63,10 @@ final class Renderer: NSObject, MTKViewDelegate { enc.setDepthStencilState(depth) enc.setCullMode(.back) enc.setFrontFacing(.counterClockwise) - enc.setVertexBuffer(cube, offset: 0, index: 0) + enc.setVertexBuffer(sceneBuffer, offset: 0, index: 0) let eyeW = size.width / 2 - let model = translation([0, 0, -1.5]) - * simd_float4x4(simd_quatf(angle: t * 0.9, axis: normalize(SIMD3<Float>(0.3, 1, 0.2)))) - * scale(0.4) + let spin = simd_float4x4(simd_quatf(angle: t * 0.9, axis: normalize(SIMD3<Float>(0.3, 1, 0.2)))) let head = simd_float4x4(headOrientation()) let proj = perspective(fovY: fovY, aspect: Float(eyeW / size.height), near: 0.05, far: 100) @@ -97,9 +75,13 @@ final class Renderer: NSObject, MTKViewDelegate { width: Double(eyeW), height: Double(size.height), znear: 0, zfar: 1)) let eyeOffset: Float = (eye == 0 ? -0.5 : 0.5) * ipd let view = (head * translation([eyeOffset, 0, 0])).inverse - var u = Uniforms(mvp: proj * view * model, model: model) - enc.setVertexBytes(&u, length: MemoryLayout<Uniforms>.stride, index: 1) - enc.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: cubeCount) + for item in items { + // Spinning items rotate about their own origin (the model translation stays put). + let model = item.spins ? item.model * spin : item.model + var u = Uniforms(mvp: proj * view * model, model: model) + enc.setVertexBytes(&u, length: MemoryLayout<Uniforms>.stride, index: 1) + enc.drawPrimitives(type: .triangle, vertexStart: item.first, vertexCount: item.count) + } } enc.endEncoding() } diff --git a/Sources/VRWorkstation/Scene.swift b/Sources/VRWorkstation/Scene.swift @@ -0,0 +1,70 @@ +// Demo scene: a spinning cube at 1.5 m, a ground grid, and reference pillars at several depths. +import simd + +/// A run of vertices in the shared scene buffer, drawn with one model matrix. +struct SceneItem { var first: Int; var count: Int; var model: simd_float4x4; var spins = false } + +/// Eye height above the floor in metres (the head sits at the origin, -Y is down). +let floorY: Float = -1.4 + +enum Scene { + /// Axis-aligned box centred at `c` with full size `s`, as 12 triangles. Nil colour = per-face colours. + static func box(center c: SIMD3<Float>, size s: SIMD3<Float>, color: SIMD3<Float>? = nil) -> [Vertex] { + let faces: [(n: SIMD3<Float>, u: SIMD3<Float>, v: SIMD3<Float>, c: SIMD3<Float>)] = [ + ([1, 0, 0], [0, 0, -1], [0, 1, 0], [0.95, 0.30, 0.25]), + ([-1, 0, 0], [0, 0, 1], [0, 1, 0], [0.25, 0.80, 0.95]), + ([0, 1, 0], [1, 0, 0], [0, 0, -1], [0.40, 0.90, 0.35]), + ([0, -1, 0], [1, 0, 0], [0, 0, 1], [0.95, 0.85, 0.30]), + ([0, 0, 1], [1, 0, 0], [0, 1, 0], [0.75, 0.40, 0.95]), + ([0, 0, -1], [-1, 0, 0], [0, 1, 0], [0.95, 0.60, 0.20]), + ] + var out: [Vertex] = [] + for f in faces { + let ctr = c + f.n * s * 0.5 + let du = f.u * s * 0.5, dv = f.v * s * 0.5 + let p: [SIMD3<Float>] = [ctr - du - dv, ctr + du - dv, ctr + du + dv, ctr - du + dv] + for i in [0, 1, 2, 0, 2, 3] { out.append(Vertex(pos: p[i], normal: f.n, color: color ?? f.c)) } + } + return out + } + + /// Builds the whole scene: one vertex array plus the items that index into it. + static func make() -> (vertices: [Vertex], items: [SceneItem]) { + var verts: [Vertex] = [], items: [SceneItem] = [] + func add(_ v: [Vertex], _ model: simd_float4x4 = matrix_identity_float4x4, spins: Bool = false) { + items.append(SceneItem(first: verts.count, count: v.count, model: model, spins: spins)) + verts += v + } + // Hero cube: unit cube scaled to 0.4 m, 1.5 m ahead at eye level, spun by the renderer. + add(box(center: .zero, size: [1, 1, 1]), translation([0, 0, -1.5]) * scale(0.4), spins: true) + + // Floor slab plus 1 m grid lines (thin flat boxes, slightly above the slab to avoid z-fighting). + let floor: SIMD3<Float> = [0.10, 0.11, 0.15], line: SIMD3<Float> = [0.35, 0.40, 0.55] + var ground = box(center: [0, floorY - 0.01, -20], size: [60, 0.02, 60], color: floor) + for i in -15...15 { + let x = Float(i) + ground += box(center: [x, floorY + 0.003, -20], size: [i == 0 ? 0.04 : 0.015, 0.006, 60], color: line) + } + for k in -9...50 { + let z = -Float(k) + ground += box(center: [0, floorY + 0.003, z], size: [30, 0.006, k == 0 ? 0.04 : 0.015], color: line) + } + add(ground) + + // Reference pillars at increasing depth, alternating sides, in distinct colours. + let pillars: [(x: Float, z: Float, c: SIMD3<Float>)] = [ + (-0.5, -0.7, [0.95, 0.35, 0.35]), (0.9, -2.5, [0.35, 0.85, 0.45]), + (-1.6, -4.0, [0.40, 0.55, 0.95]), (2.2, -6.0, [0.95, 0.80, 0.30]), + (-3.0, -9.0, [0.85, 0.40, 0.90]), (4.0, -14.0, [0.30, 0.85, 0.85]), + ] + for p in pillars { + let h: Float = 1.0 // 1 m tall pillars standing on the floor + add(box(center: [p.x, floorY + h / 2, p.z], size: [0.25, h, 0.25], color: p.c)) + } + // Small cubes floating at head height, near and far, to compare parallax at the same height. + add(box(center: [0.35, 0.1, -0.5], size: [0.08, 0.08, 0.08])) + add(box(center: [-1.2, 0.1, -3.0], size: [0.3, 0.3, 0.3])) + add(box(center: [1.8, 0.1, -8.0], size: [0.8, 0.8, 0.8])) + return (verts, items) + } +} diff --git a/Sources/VRWorkstation/main.swift b/Sources/VRWorkstation/main.swift @@ -1,13 +1,26 @@ import Foundation import MetalKit +import simd setvbuf(stdout, nil, _IOLBF, 0) -// Usage: VRWorkstation [--windowed] | --snapshot out.png +// Usage: VRWorkstation [--windowed] | --snapshot out.png [--snapshot-size WxH] [--snapshot-yaw deg] +// Options: --snapshot-size WxH (default 4320x2160), --snapshot-yaw <degrees> (head turn, positive = left). +func option(_ name: String) -> String? { + guard let i = CommandLine.arguments.firstIndex(of: name), CommandLine.arguments.count > i + 1 else { return nil } + return CommandLine.arguments[i + 1] +} if let i = CommandLine.arguments.firstIndex(of: "--snapshot") { let path = CommandLine.arguments.count > i + 1 ? CommandLine.arguments[i + 1] : "snapshot.png" + let dims = (option("--snapshot-size") ?? "4320x2160").split(separator: "x").compactMap { Int($0) } + let (w, h) = dims.count == 2 ? (dims[0], dims[1]) : (4320, 2160) + let yaw = Float(option("--snapshot-yaw") ?? "0") ?? 0 let view = MTKView(frame: .zero, device: MTLCreateSystemDefaultDevice()) view.colorPixelFormat = .bgra8Unorm; view.depthStencilPixelFormat = .depth32Float - do { try Renderer(view: view).snapshot(width: 2160, height: 1080, time: 1.0, to: path); print("wrote", path) } - catch { print("snapshot failed:", error) } + do { + let r = try Renderer(view: view) + r.headOrientation = { simd_quatf(angle: yaw * .pi / 180, axis: [0, 1, 0]) } + try r.snapshot(width: w, height: h, time: 1.0, to: path) + print("wrote", path) + } catch { print("snapshot failed:", error) } exit(0) } runApp(windowed: CommandLine.arguments.contains("--windowed")) diff --git a/tools/hpmctl.c b/tools/hpmctl.c @@ -0,0 +1,76 @@ +// hpmctl: talk to the Mac's USB-C port controllers (Apple "Ace" / CD3217, TI TPS6598x family) through +// Apple's AppleHPMLib IOKit plug-in, the same path /usr/bin/hpmdiagnose uses. +// +// Usage: hpmctl <rid> read <reg> read one controller register (hex), e.g. `hpmctl 1 read 0x58` +// +// RID 0 = Port-USB-C@1 (hub, headset USB), RID 1 = Port-USB-C@2 (DP adapter) on this MacBook Air. +// Plug-in function table layout (from dyld_info fixups of AppleHPMLib, macOS 15.6.1): +// +0x08 QueryInterface +0x10 AddRef +0x18 Release +0x28 iecsRead +0x30 iecsWrite +0x38 iecsCommand +// +0x40 sendVDM +0x48 receiveVDM +0x50 receiveVDMAttention +// Build: clang -O2 -framework IOKit -framework CoreFoundation hpmctl.c -o hpmctl +#include <CoreFoundation/CoreFoundation.h> +#include <IOKit/IOCFPlugIn.h> +#include <IOKit/IOKitLib.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +typedef struct HPMInterface { + IUNKNOWN_C_GUTS; + uint16_t version, revision; + IOReturn (*iecsRead)(void *self, uint64_t route, uint8_t reg, void *buf, uint64_t len, uint32_t flags, uint64_t *outLen); + IOReturn (*iecsWrite)(void *self, uint64_t route, uint8_t reg, const void *buf, uint64_t len, uint32_t flags); + IOReturn (*iecsCommand)(void *self, uint64_t route, uint32_t cmd4cc, uint32_t flags); +} HPMInterface; + +// Plug-in type and HPM interface IDs, from AppleHPMLib's Info.plist and queryInterfaceStatic. +#define kHPMPlugInTypeID CFUUIDGetConstantUUIDWithBytes(NULL, 0x12, 0xA1, 0xDC, 0xCF, 0xCF, 0x7A, 0x47, 0x75, \ + 0xBE, 0xE5, 0x9C, 0x43, 0x19, 0xF4, 0xCD, 0x2B) +#define kHPMInterfaceID CFUUIDGetConstantUUIDWithBytes(NULL, 0xC1, 0x3A, 0xCD, 0xD9, 0x20, 0x9E, 0x4B, 0x01, \ + 0xB7, 0xBE, 0xE0, 0x5C, 0xD8, 0x83, 0xC7, 0xB1) + +/// Finds the AppleHPM bus service whose child AppleHPMDevice has the given RID, and opens the plug-in on it. +static HPMInterface **openHPM(int rid) { + io_iterator_t it; + if (IOServiceGetMatchingServices(kIOMainPortDefault, IOServiceMatching("AppleHPMDevice"), &it) != KERN_SUCCESS) return NULL; + io_service_t dev, found = 0; + while (!found && (dev = IOIteratorNext(it))) { + CFNumberRef n = IORegistryEntryCreateCFProperty(dev, CFSTR("RID"), NULL, 0); + int v = -1; + if (n) { CFNumberGetValue(n, kCFNumberIntType, &v); CFRelease(n); } + if (v == rid) IORegistryEntryGetParentEntry(dev, kIOServicePlane, &found); // AppleHPMARMI2C + IOObjectRelease(dev); + } + IOObjectRelease(it); + if (!found) { fprintf(stderr, "no AppleHPMDevice with RID %d\n", rid); return NULL; } + + IOCFPlugInInterface **plugin = NULL; + SInt32 score; + kern_return_t kr = IOCreatePlugInInterfaceForService(found, kHPMPlugInTypeID, kIOCFPlugInInterfaceID, &plugin, &score); + IOObjectRelease(found); + if (kr != KERN_SUCCESS || !plugin) { fprintf(stderr, "IOCreatePlugInInterfaceForService: 0x%x\n", kr); return NULL; } + HPMInterface **hpm = NULL; + HRESULT hr = (*plugin)->QueryInterface(plugin, CFUUIDGetUUIDBytes(kHPMInterfaceID), (LPVOID *)&hpm); + (*plugin)->Release(plugin); + if (hr != S_OK || !hpm) { fprintf(stderr, "QueryInterface: 0x%x\n", (int)hr); return NULL; } + return hpm; +} + +int main(int argc, char **argv) { + if (argc < 4 || strcmp(argv[2], "read") != 0) { fprintf(stderr, "usage: hpmctl <rid> read <reg>\n"); return 2; } + int rid = atoi(argv[1]); + uint8_t reg = (uint8_t)strtoul(argv[3], NULL, 0); + HPMInterface **hpm = openHPM(rid); + if (!hpm) return 1; + uint8_t buf[64] = {0}; + uint64_t got = 0; + IOReturn r = (*hpm)->iecsRead(hpm, 0, reg, buf, sizeof buf, 0, &got); + if (r != kIOReturnSuccess) { fprintf(stderr, "iecsRead(0x%02x): 0x%08x\n", reg, r); return 1; } + printf("rid %d reg 0x%02x len %llu:", rid, reg, got); + for (uint64_t i = 0; i < got; i++) printf(" %02x", buf[i]); + printf(" \""); + for (uint64_t i = 0; i < got; i++) putchar(buf[i] >= 32 && buf[i] < 127 ? buf[i] : '.'); + printf("\"\n"); + (*hpm)->Release(hpm); + return 0; +}