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:
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;
+}