‹ 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 7feda948f29af1c3618c82d5020e993fa4a1168e
parent 60e27f974db118a726215c51345505f6d8fc8874
Author: umhau <umhau@example.invalid>
Date:   Wed, 21 Oct 2026 12:00:22 +0000

Add live optical tuning and filter the floor grid at pixel scale

Diffstat:
MNOTES.md | 22++++++++++++++++++++++
MSETUP.md | 8++++++++
MSources/VRWorkstation/App.swift | 5+++++
MSources/VRWorkstation/LensWarp.swift | 36++++++++++++++++++++++++------------
ASources/VRWorkstation/OpticsControls.swift | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MSources/VRWorkstation/Renderer.swift | 30+++++++++++++++++++++++-------
MSources/VRWorkstation/Scene.swift | 21++++++---------------
MSources/VRWorkstation/Shaders.swift | 20+++++++++++++++++---
MTests/LensWarpChecks.swift | 27++++++++++++++++++++++++++-
Aevidence/lenswarp-tuning-checks.txt | 2++
Aevidence/optics-controls-live.txt | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aevidence/optics-user-confirmed-checkpoint.json | 5+++++
12 files changed, 334 insertions(+), 38 deletions(-)

diff --git a/NOTES.md b/NOTES.md @@ -124,3 +124,25 @@ The calibrated live build also passed a forced panel-off recovery: display ADD a 0.654 s post-reset retrain healed automatically; native 90 Hz rendering resumed with no GPU errors. See `evidence/calibrated-recovery-trial.json` and `evidence/calibrated-recovery-live.txt`. The running demo logs to `/tmp/vr-calibrated-live.txt`; Q or Escape in its window quits it. + +## Live optical tuning and floor-grid filtering (2026-10-21) + +The Mac has a Headset optics window with +scene/white-grid/green-grid patterns, red strength, and separate horizontal/vertical blue strengths (-100% to 400%). +100% is the original factory compensation; zero uses green geometry for that channel. The controls pass settings to +the render worker under a lock, and debounce-save them to Application Support/VRWorkstation/optics-adjustments.json. +Factory JSON remains unchanged. Green-only mode disables the color sliders. Reset restores factory color strengths. + +Separately, the floor's 1.5 cm geometric grid strips projected below one panel pixel at distance. They were replaced +with one floor surface and a derivative-filtered grid in its fragment shader: minimum readable pixel footprint, +smoothed coverage, and per-axis fading when line spacing becomes unresolved. This reduces different aliasing between +the two eyes, but does not change the stereo view matrices. An isolated infinity-alignment target was discussed but +has NOT been added; the white/green patterns are angular grids. + +Operator feedback confirmed an improvement with optical tuning. Personal adjustment values and verbatim feedback are omitted. +Current evidence/optics-controls-live.txt shows approximately 90 presented fps and zero GPU errors. + +The optical tuning shader passes float-coordinate comparisons including independent blue axes; the test also retains +the end-to-end quantized sampling check. Screenshots show pre-compensated output before the physical lenses, so they +cannot establish the blur/fringe that reaches a user's eye. Software rendering, optical compensation, and physical +lens/eye placement are separate contributors; do not infer a hardware defect from the remaining residuals. diff --git a/SETUP.md b/SETUP.md @@ -148,3 +148,11 @@ zsh tools/build.sh # builds the app + the signe - `verify.sh`: passive one-shot check (boot-args + summaries of existing logs). No ioreg/system_profiler/HID. Launches nothing. - `build.sh`: builds the app and the signed `hpmctl` helper beside it (SETUP step 5). - `hpmctl` / `recovery_trials.py` / the app's LinkRecovery helper: see NOTES.md "Link reliability" (vr0 owns these). + +### Optical tuning window + +The **Headset optics** window on the Mac switches between the 3D scene, a stationary white grid, and a green-only +sharpness grid. Red correction and separate blue horizontal/vertical correction range from -100% to 400%; 100% is +factory compensation. Changes save automatically to `~/Library/Application Support/VRWorkstation/optics-adjustments.json`. +The factory reset button restores 100% for all three controls. Color controls are disabled for the green-only pattern. +The floor now uses a filtered procedural grid to avoid subpixel line aliasing at distance. diff --git a/Sources/VRWorkstation/App.swift b/Sources/VRWorkstation/App.swift @@ -56,6 +56,7 @@ final class HeadsetSession: NSObject, NSApplicationDelegate { let view: MetalSurface private let renderLoop: MetalRenderLoop private var window: HeadsetWindow? + private var opticsControls: OpticsControls? private(set) var display: CGDirectDisplayID? private var lostAt: Date? private var hidRetry: Timer? @@ -115,6 +116,8 @@ final class HeadsetSession: NSObject, NSApplicationDelegate { } } + func showOpticsControls() { opticsControls = OpticsControls(renderer: renderer) } + private func startHID() { guard !stopping else { return } do { @@ -151,6 +154,7 @@ final class HeadsetSession: NSObject, NSApplicationDelegate { } hmd.stop() window?.orderOut(nil) + opticsControls?.close(); opticsControls = nil if let display { CGDisplayRelease(display) } display = nil } @@ -282,6 +286,7 @@ func runApp() { } session.stopAfter(seconds) } + session.showOpticsControls() app.activate(ignoringOtherApps: true) signal(SIGINT, SIG_IGN); signal(SIGTERM, SIG_IGN) let signals = [SIGINT, SIGTERM].map { value -> DispatchSourceSignal in diff --git a/Sources/VRWorkstation/LensWarp.swift b/Sources/VRWorkstation/LensWarp.swift @@ -42,6 +42,7 @@ private struct WarpUniforms { var k3: SIMD4<Float> var ranges: SIMD4<Float> var panel: SIMD4<Float> + var tuning: SIMD4<Float> } /// Render the scene to an oversampled target, then correct geometry and each color separately. @@ -87,19 +88,18 @@ final class LensWarp { uniforms.append(WarpUniforms(inverseAffine: inverse, red: channel(0), green: channel(1), blue: channel(2), k3: SIMD4(Float(eye.channels[0].k.z), Float(eye.channels[1].k.z), Float(eye.channels[2].k.z), 0), ranges: SIMD4(Float(b.texXRange.x), Float(b.texXRange.y), Float(b.texYRange.x), Float(b.texYRange.y)), - panel: SIMD4(4320, 2160, Float(index), Float(width / 2)))) + panel: SIMD4(4320, 2160, Float(index), Float(width / 2)), tuning: SIMD4(1, 1, 1, 0))) } } #if LENS_WARP_CHECKS /// Test-only float output from the exact production shader function, before sampling/quantization. - func coordinateSamples(device: MTLDevice, points: [SIMD2<Float>], eye: Int) throws -> [SIMD2<Float>] { + func coordinateSamples(device: MTLDevice, points: [SIMD2<Float>], eye: Int, redGain: Float = 1, blueHorizontalGain: Float = 1, blueVerticalGain: Float = 1) throws -> [SIMD2<Float>] { let kernel = """ kernel void check_coordinates(constant WarpUniforms &u [[buffer(0)]], device const float2 *points [[buffer(1)]], device float2 *out [[buffer(2)]], uint i [[thread_position_in_grid]]) { - out[3*i] = channel_uv(points[i], u.red, u.k3.x, u); - out[3*i+1] = channel_uv(points[i], u.green, u.k3.y, u); - out[3*i+2] = channel_uv(points[i], u.blue, u.k3.z, u); + ColorCoordinates tc = corrected_coordinates(points[i], u); + out[3*i] = tc.r; out[3*i+1] = tc.g; out[3*i+2] = tc.b; } """ let lib = try device.makeLibrary(source: lensWarpShader + kernel, options: nil) @@ -109,6 +109,7 @@ final class LensWarp { let command = device.makeCommandQueue()!.makeCommandBuffer()! let enc = command.makeComputeCommandEncoder()! var u = uniforms[eye] + u.tuning = SIMD4(redGain, blueHorizontalGain, blueVerticalGain, 0) enc.setComputePipelineState(pipeline) enc.setBytes(&u, length: MemoryLayout<WarpUniforms>.stride, index: 0) enc.setBuffer(input, offset: 0, index: 1) @@ -120,12 +121,14 @@ final class LensWarp { } #endif - func encodeGrid(_ cb: MTLCommandBuffer, pass: MTLRenderPassDescriptor) { + func encodeGrid(_ cb: MTLCommandBuffer, pass: MTLRenderPassDescriptor, greenOnly: Bool = false) { guard let enc = cb.makeRenderCommandEncoder(descriptor: pass) else { return } enc.setRenderPipelineState(gridPipeline) for eye in 0..<2 { enc.setViewport(MTLViewport(originX: Double(color.width / 2 * eye), originY: 0, width: Double(color.width / 2), height: Double(color.height), znear: 0, zfar: 1)) + var green: UInt32 = greenOnly ? 1 : 0 + enc.setFragmentBytes(&green, length: MemoryLayout<UInt32>.stride, index: 1) var ranges = uniforms[eye].ranges enc.setFragmentBytes(&ranges, length: MemoryLayout<SIMD4<Float>>.stride, index: 0) enc.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3) @@ -133,7 +136,7 @@ final class LensWarp { enc.endEncoding() } - func encode(_ cb: MTLCommandBuffer, target: MTLTexture) { + func encode(_ cb: MTLCommandBuffer, target: MTLTexture, redGain: Float = 1, blueHorizontalGain: Float = 1, blueVerticalGain: Float = 1) { let pass = MTLRenderPassDescriptor() pass.colorAttachments[0].texture = target pass.colorAttachments[0].loadAction = .clear @@ -146,6 +149,7 @@ final class LensWarp { enc.setViewport(MTLViewport(originX: Double(target.width / 2 * eye), originY: 0, width: Double(target.width / 2), height: Double(target.height), znear: 0, zfar: 1)) var u = uniforms[eye] + u.tuning = SIMD4(redGain, blueHorizontalGain, blueVerticalGain, 0) enc.setFragmentBytes(&u, length: MemoryLayout<WarpUniforms>.stride, index: 0) enc.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3) } @@ -158,7 +162,7 @@ private let lensWarpShader = """ using namespace metal; struct WarpUniforms { float3x3 inverseAffine; - float4 red, green, blue, k3, ranges, panel; + float4 red, green, blue, k3, ranges, panel, tuning; }; struct WarpVertex { float4 position [[position]]; float2 uv; }; vertex WarpVertex warp_vertex(uint id [[vertex_id]]) { @@ -181,13 +185,20 @@ float color_sample(texture2d<float> source, float2 tc, uint channel, constant Wa constexpr sampler linearSampler(coord::normalized, address::clamp_to_edge, filter::linear); return source.sample(linearSampler, tc)[channel]; } +struct ColorCoordinates { float2 r, g, b; }; +ColorCoordinates corrected_coordinates(float2 uv, constant WarpUniforms &u) { + ColorCoordinates tc; + tc.g = channel_uv(uv, u.green, u.k3.y, u); + tc.r = mix(tc.g, channel_uv(uv, u.red, u.k3.x, u), u.tuning.x); + tc.b = mix(tc.g, channel_uv(uv, u.blue, u.k3.z, u), u.tuning.yz); + return tc; +} fragment float4 warp_rgb(WarpVertex in [[stage_in]], texture2d<float> source [[texture(0)]], constant WarpUniforms &u [[buffer(0)]]) { - float2 r = channel_uv(in.uv, u.red, u.k3.x, u); - float2 g = channel_uv(in.uv, u.green, u.k3.y, u); - float2 b = channel_uv(in.uv, u.blue, u.k3.z, u); + ColorCoordinates tc = corrected_coordinates(in.uv, u); + float2 r = tc.r, g = tc.g, b = tc.b; return float4(color_sample(source, r, 0, u), color_sample(source, g, 1, u), color_sample(source, b, 2, u), 1); } -fragment float4 optics_grid(WarpVertex in [[stage_in]], constant float4 &ranges [[buffer(0)]]) { +fragment float4 optics_grid(WarpVertex in [[stage_in]], constant float4 &ranges [[buffer(0)]], constant uint &greenOnly [[buffer(1)]]) { float2 tangent = mix(ranges.xz, ranges.yw, in.uv); float2 cell = abs(fract(tangent * 10 + .5) - .5); float2 aa = fwidth(tangent * 10); @@ -195,6 +206,7 @@ fragment float4 optics_grid(WarpVertex in [[stage_in]], constant float4 &ranges float cross = min(abs(tangent.x), abs(tangent.y)); float3 color = mix(float3(.01), float3(.8), line); if (cross < .001) color = float3(0, .8, .2); + if (greenOnly) color *= float3(0, 1, 0); return float4(color, 1); } """ diff --git a/Sources/VRWorkstation/OpticsControls.swift b/Sources/VRWorkstation/OpticsControls.swift @@ -0,0 +1,111 @@ +import AppKit + +struct OpticalSettings: Equatable, Codable { + enum Pattern: Int, Codable { case scene, whiteGrid, greenGrid } + var pattern: Pattern = .scene + var redGain: Float = 1 + var blueHorizontalGain: Float = 1 + var blueVerticalGain: Float = 1 + + static var cacheURL: URL { HeadsetOptics.cacheURL.deletingLastPathComponent().appendingPathComponent("optics-adjustments.json") } + static func initial() -> OpticalSettings { + var settings = (try? JSONDecoder().decode(OpticalSettings.self, from: Data(contentsOf: cacheURL))) ?? OpticalSettings() + settings.redGain = settings.redGain.isFinite ? min(4, max(-1, settings.redGain)) : 1 + settings.blueHorizontalGain = settings.blueHorizontalGain.isFinite ? min(4, max(-1, settings.blueHorizontalGain)) : 1 + settings.blueVerticalGain = settings.blueVerticalGain.isFinite ? min(4, max(-1, settings.blueVerticalGain)) : 1 + if CommandLine.arguments.contains("--green-grid") { settings.pattern = .greenGrid } + else if CommandLine.arguments.contains("--optics-grid") { settings.pattern = .whiteGrid } + return settings + } + + func save() { + do { try JSONEncoder().encode(self).write(to: Self.cacheURL, options: .atomic) } + catch { log("cannot save optical adjustments: \(error)") } + } +} + +/// Main-screen controls; the render worker receives settings through Renderer's lock. +final class OpticsControls: NSObject { + private let renderer: Renderer + private let window: NSWindow + private let pattern = NSPopUpButton() + private let red = NSSlider(value: 1, minValue: -1, maxValue: 4, target: nil, action: nil) + private let blueHorizontal = NSSlider(value: 1, minValue: -1, maxValue: 4, target: nil, action: nil) + private let blueVertical = NSSlider(value: 1, minValue: -1, maxValue: 4, target: nil, action: nil) + private let redLabel = NSTextField(labelWithString: "") + private var saveTimer: Timer? + private let blueHorizontalLabel = NSTextField(labelWithString: "") + + private let blueVerticalLabel = NSTextField(labelWithString: "") + + init(renderer: Renderer) { + self.renderer = renderer + window = NSWindow(contentRect: NSRect(x: 60, y: 100, width: 460, height: 340), + styleMask: [.titled, .miniaturizable], backing: .buffered, defer: false) + super.init() + window.title = "Headset optics" + window.isReleasedWhenClosed = false + let stack = NSStackView() + stack.orientation = .vertical; stack.alignment = .leading; stack.spacing = 12 + stack.edgeInsets = NSEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) + stack.translatesAutoresizingMaskIntoConstraints = false + window.contentView!.addSubview(stack) + NSLayoutConstraint.activate([ + stack.leadingAnchor.constraint(equalTo: window.contentView!.leadingAnchor), + stack.trailingAnchor.constraint(equalTo: window.contentView!.trailingAnchor), + stack.topAnchor.constraint(equalTo: window.contentView!.topAnchor) + ]) + pattern.addItems(withTitles: ["3D scene", "White grid — sharpness and color", "Green grid — sharpness only"]) + pattern.target = self; pattern.action = #selector(changed) + stack.addArrangedSubview(pattern) + let help = NSTextField(wrappingLabelWithString: "Compare both line directions on the stationary grids. Try one eye at a time; the green cross marks straight ahead.") + stack.addArrangedSubview(help) + for (slider, label) in [(red, redLabel), (blueHorizontal, blueHorizontalLabel), (blueVertical, blueVerticalLabel)] { + slider.target = self; slider.action = #selector(changed) + slider.isContinuous = true + slider.widthAnchor.constraint(equalToConstant: 410).isActive = true + stack.addArrangedSubview(label); stack.addArrangedSubview(slider) + } + stack.addArrangedSubview(NSTextField(labelWithString: "Color correction: 0% = off, 100% = factory")) + let reset = NSButton(title: "Reset colors to factory", target: self, action: #selector(resetColors)) + stack.addArrangedSubview(reset) + let settings = renderer.opticalSettings() + pattern.selectItem(at: settings.pattern.rawValue) + red.floatValue = settings.redGain + blueHorizontal.floatValue = settings.blueHorizontalGain + blueVertical.floatValue = settings.blueVerticalGain + refreshLabels() + window.setContentSize(NSSize(width: 460, height: max(340, stack.fittingSize.height))) + if let screen = NSScreen.screens.first(where: { ($0.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber)?.uint32Value == CGMainDisplayID() }) { + window.setFrameOrigin(NSPoint(x: screen.visibleFrame.minX + 60, y: screen.visibleFrame.maxY - window.frame.height - 60)) + } + window.makeKeyAndOrderFront(nil) + } + + @objc private func changed() { + var settings = renderer.opticalSettings() + settings.pattern = OpticalSettings.Pattern(rawValue: pattern.indexOfSelectedItem) ?? .scene + settings.redGain = (red.floatValue * 100).rounded() / 100 + settings.blueHorizontalGain = (blueHorizontal.floatValue * 100).rounded() / 100 + settings.blueVerticalGain = (blueVertical.floatValue * 100).rounded() / 100 + renderer.setOpticalSettings(settings) + saveTimer?.invalidate() + saveTimer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: false) { _ in settings.save() } + refreshLabels() + } + + private func refreshLabels() { + let colorsVisible = pattern.indexOfSelectedItem != OpticalSettings.Pattern.greenGrid.rawValue + red.isEnabled = colorsVisible; blueHorizontal.isEnabled = colorsVisible; blueVertical.isEnabled = colorsVisible + redLabel.stringValue = String(format: "Red correction: %.0f%%", red.floatValue * 100) + blueHorizontalLabel.stringValue = String(format: "Blue horizontal correction (sides): %.0f%%", blueHorizontal.floatValue * 100) + blueVerticalLabel.stringValue = String(format: "Blue vertical correction (top/bottom): %.0f%%", blueVertical.floatValue * 100) + } + + @objc private func resetColors() { red.floatValue = 1; blueHorizontal.floatValue = 1; blueVertical.floatValue = 1; changed() } + func close() { + saveTimer?.invalidate(); saveTimer = nil + renderer.opticalSettings().save() + window.orderOut(nil) + } +} diff --git a/Sources/VRWorkstation/Renderer.swift b/Sources/VRWorkstation/Renderer.swift @@ -5,7 +5,7 @@ import QuartzCore import simd struct Vertex { var pos: SIMD3<Float>; var normal: SIMD3<Float>; var color: SIMD3<Float> } -struct Uniforms { var mvp: simd_float4x4; var model: simd_float4x4 } +struct Uniforms { var mvp: simd_float4x4; var model: simd_float4x4; var material: SIMD4<Float> } struct FrameMetrics { var presented = 0 @@ -27,7 +27,21 @@ final class Renderer: NSObject, CAMetalDisplayLinkDelegate { let start = CACurrentMediaTime() let optics: HeadsetOptics private let lensWarp: LensWarp - private let opticsGrid = CommandLine.arguments.contains("--optics-grid") + private let settingsLock = NSLock() + private var settings = OpticalSettings.initial() + + func opticalSettings() -> OpticalSettings { + settingsLock.lock(); defer { settingsLock.unlock() } + return settings + } + + func setOpticalSettings(_ value: OpticalSettings) { + settingsLock.lock() + guard settings != value else { settingsLock.unlock(); return } + settings = value; settingsLock.unlock() + + // Keep personal optical settings out of diagnostic logs. + } /// Head orientation (world-from-head). Identity when head tracking is off. var headOrientation: () -> simd_quatf = { simd_quatf(angle: 0, axis: [0, 1, 0]) } @@ -86,10 +100,11 @@ final class Renderer: NSObject, CAMetalDisplayLinkDelegate { metricsLock.unlock() if frame == 1 { log("render worker main=\(Thread.isMainThread)") } if frame == 1 || frame % 300 == 0 { print("frame \(frame) drawable \(width)x\(height)") } - if opticsGrid { lensWarp.encodeGrid(cb, pass: rpd) } else { + let settings = opticalSettings() + if settings.pattern != .scene { lensWarp.encodeGrid(cb, pass: rpd, greenOnly: settings.pattern == .greenGrid) } else { encode(cb, rpd, size: CGSize(width: lensWarp.color.width, height: lensWarp.color.height), time: Float(presentationTime - start)) } - lensWarp.encode(cb, target: drawable.texture) + lensWarp.encode(cb, target: drawable.texture, redGain: settings.redGain, blueHorizontalGain: settings.blueHorizontalGain, blueVerticalGain: settings.blueVerticalGain) drawable.addPresentedHandler { [weak self] presented in guard let self else { return } self.metricsLock.lock(); defer { self.metricsLock.unlock() } @@ -136,7 +151,7 @@ final class Renderer: NSObject, CAMetalDisplayLinkDelegate { 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) + var u = Uniforms(mvp: proj * view * model, model: model, material: SIMD4(item.floorGrid ? 1 : 0, 0, 0, 0)) enc.setVertexBytes(&u, length: MemoryLayout<Uniforms>.stride, index: 1) enc.drawPrimitives(type: .triangle, vertexStart: item.first, vertexCount: item.count) } @@ -160,10 +175,11 @@ final class Renderer: NSObject, CAMetalDisplayLinkDelegate { rpd.depthAttachment.loadAction = .clear rpd.depthAttachment.clearDepth = 1 let cb = queue.makeCommandBuffer()! - if opticsGrid { lensWarp.encodeGrid(cb, pass: rpd) } else { + let settings = opticalSettings() + if settings.pattern != .scene { lensWarp.encodeGrid(cb, pass: rpd, greenOnly: settings.pattern == .greenGrid) } else { encode(cb, rpd, size: CGSize(width: lensWarp.color.width, height: lensWarp.color.height), time: time) } - lensWarp.encode(cb, target: color) + lensWarp.encode(cb, target: color, redGain: settings.redGain, blueHorizontalGain: settings.blueHorizontalGain, blueVerticalGain: settings.blueVerticalGain) cb.commit(); cb.waitUntilCompleted() guard cb.status == .completed else { throw cb.error ?? NSError(domain: "Renderer", code: 2, userInfo: [NSLocalizedDescriptionKey: "Snapshot GPU command failed"]) diff --git a/Sources/VRWorkstation/Scene.swift b/Sources/VRWorkstation/Scene.swift @@ -2,7 +2,7 @@ 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 } +struct SceneItem { var first: Int; var count: Int; var model: simd_float4x4; var spins = false; var floorGrid = false } /// Eye height above the floor in metres (the head sits at the origin, -Y is down). let floorY: Float = -1.4 @@ -31,25 +31,16 @@ enum Scene { /// 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)) + func add(_ v: [Vertex], _ model: simd_float4x4 = matrix_identity_float4x4, spins: Bool = false, floorGrid: Bool = false) { + items.append(SceneItem(first: verts.count, count: v.count, model: model, spins: spins, floorGrid: floorGrid)) 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) + // One floor surface; the fragment shader draws a pixel-filtered metric grid. + // Thin geometry otherwise compresses the transverse lines below one panel pixel at distance. + add(box(center: [0, floorY - 0.01, -20], size: [60, 0.02, 60], color: [0.10, 0.11, 0.15]), floorGrid: true) // Reference pillars at increasing depth, alternating sides, in distinct colours. let pillars: [(x: Float, z: Float, c: SIMD3<Float>)] = [ diff --git a/Sources/VRWorkstation/Shaders.swift b/Sources/VRWorkstation/Shaders.swift @@ -4,20 +4,34 @@ let shaderSource = """ using namespace metal; struct Vertex { float3 pos; float3 normal; float3 color; }; // 16-byte aligned, matches Swift SIMD3<Float> -struct Uniforms { float4x4 mvp; float4x4 model; }; -struct VOut { float4 pos [[position]]; float3 normal; float3 color; }; +struct Uniforms { float4x4 mvp; float4x4 model; float4 material; }; +struct VOut { float4 pos [[position]]; float3 normal; float3 color; float3 world; float grid [[flat]]; }; vertex VOut vmain(uint vid [[vertex_id]], const device Vertex *v [[buffer(0)]], constant Uniforms &u [[buffer(1)]]) { VOut o; o.pos = u.mvp * float4(v[vid].pos, 1); o.normal = (u.model * float4(v[vid].normal, 0)).xyz; o.color = v[vid].color; + o.world = (u.model * float4(v[vid].pos, 1)).xyz; + o.grid = u.material.x; return o; } fragment float4 fmain(VOut in [[stage_in]]) { float3 l = normalize(float3(0.4, 0.8, 0.6)); float d = max(dot(normalize(in.normal), l), 0.0); - return float4(in.color * (0.25 + 0.75 * d), 1); + float3 color = in.color; + if (in.grid > .5 && in.normal.y > .5) { + float2 p = in.world.xz; + float2 footprint = max(fwidth(p), float2(1e-5)); + float2 distanceToLine = abs(fract(p + .5) - .5); + // Maintain a readable minimum footprint, then fade when adjacent lines become unresolved. + float2 halfWidth = max(float2(.0075), footprint * .8); + float2 coverage = 1 - smoothstep(halfWidth - footprint * .5, halfWidth + footprint * .5, distanceToLine); + float2 resolvable = 1 - smoothstep(float2(.20), float2(.45), footprint); + float line = max(coverage.x * resolvable.x, coverage.y * resolvable.y); + color = mix(color, float3(.35, .40, .55), line); + } + return float4(color * (0.25 + 0.75 * d), 1); } """ diff --git a/Tests/LensWarpChecks.swift b/Tests/LensWarpChecks.swift @@ -28,8 +28,33 @@ struct LensWarpChecks { } } } + for gain: Float in [-1, 0, 0.5, 2, 4] { + for eye in 0..<2 { + let actual = try warp.coordinateSamples(device: device, points: points, eye: eye, redGain: gain, blueHorizontalGain: gain, blueVerticalGain: gain) + for (i, p) in points.enumerated() { + let cpu = optics.calibration.eyes[eye].distort(view: eye, u: Double(p.x), v: Double(p.y), bounds: optics.bounds[eye]) + let expected = [cpu.g + (cpu.r - cpu.g) * Double(gain), cpu.g, cpu.g + (cpu.b - cpu.g) * Double(gain)] + for c in 0..<3 { + let error = simd_abs(SIMD2<Double>(actual[i * 3 + c]) - expected[c]) + coordinateWorst = max(coordinateWorst, error.x, error.y) + } + } + } + } + for eye in 0..<2 { + let actual = try warp.coordinateSamples(device: device, points: points, eye: eye, redGain: 1.75, blueHorizontalGain: 2.25, blueVerticalGain: 1.5) + for (i, p) in points.enumerated() { + let cpu = optics.calibration.eyes[eye].distort(view: eye, u: Double(p.x), v: Double(p.y), bounds: optics.bounds[eye]) + let expected = [cpu.g + (cpu.r - cpu.g) * Double(Float(1.75)), cpu.g, + cpu.g + (cpu.b - cpu.g) * SIMD2(Double(Float(2.25)), Double(Float(1.5)))] + for c in 0..<3 { + let error = simd_abs(SIMD2<Double>(actual[i * 3 + c]) - expected[c]) + coordinateWorst = max(coordinateWorst, error.x, error.y) + } + } + } precondition(coordinateWorst < 1e-5, "Float shader coordinates differ from Double model: \(coordinateWorst)") - print("PASS: \(points.count * 2 * 3) RGB float-coordinate pairs, max GPU/CPU error \(coordinateWorst)") + print("PASS: \(points.count * 2 * 3 * 7) RGB float-coordinate pairs (six strengths and independent blue axes), max GPU/CPU error \(coordinateWorst)") let w = warp.color.width, h = warp.color.height, eyeW = w / 2 let bytes = UnsafeMutablePointer<UInt8>.allocate(capacity: w * h * 4) defer { bytes.deallocate() } diff --git a/evidence/lenswarp-tuning-checks.txt b/evidence/lenswarp-tuning-checks.txt @@ -0,0 +1,2 @@ +PASS: 26250 RGB float-coordinate pairs (six strengths and independent blue axes), max GPU/CPU error 1.7778522127809282e-06 +PASS: 1632 RGB channel samples, max GPU/CPU error 0.0041510994889600375; asymmetric projection edges correct diff --git a/evidence/optics-controls-live.txt b/evidence/optics-controls-live.txt @@ -0,0 +1,85 @@ +7851.892422 [hmd] serial REDACTED-DEVICE-SERIAL +7852.001422 [hmd] wake commands accepted; waiting for IMU and video evidence +7852.018422 captured display 2: ok +7852.532422 [display callback] id=2 flags=8 main=true +7852.545422 mode 4320x2160@90 (ui 4320x2160): ok +7852.561422 rendering on headset (4320.0, 2160.0), drawable (4320.0, 2160.0); dedicated Metal display loop at 90 Hz +7852.570422 render worker main=false +frame 1 drawable 4320x2160 +7852.651422 [display callback] handled after 118.5 ms +7853.871422 [hmd] display ready=true; raw 05 01 01 01 01 59 00 00 00 07 3b +frame 300 drawable 4320x2160 +frame 600 drawable 4320x2160 +7862.003422 health display=2 submitted=843 presented=833 skipped=7 fps=77.81 presentAge=0.00s GPUms=6.56 GPUerrors=0 IMU=9504 lastIMU=0.0s status=1 +frame 900 drawable 4320x2160 +frame 1200 drawable 4320x2160 +frame 1500 drawable 4320x2160 +7872.002422 health display=2 submitted=1743 presented=1733 skipped=7 fps=90.00 presentAge=0.00s GPUms=6.58 GPUerrors=0 IMU=19504 lastIMU=0.0s status=1 +frame 1800 drawable 4320x2160 +frame 2100 drawable 4320x2160 +frame 2400 drawable 4320x2160 +7882.002422 health display=2 submitted=2643 presented=2633 skipped=7 fps=90.00 presentAge=0.00s GPUms=6.59 GPUerrors=0 IMU=29508 lastIMU=0.0s status=1 +frame 2700 drawable 4320x2160 +frame 3000 drawable 4320x2160 +frame 3300 drawable 4320x2160 +7892.002422 health display=2 submitted=3543 presented=3533 skipped=7 fps=90.00 presentAge=0.00s GPUms=6.59 GPUerrors=0 IMU=39512 lastIMU=0.0s status=1 +frame 3600 drawable 4320x2160 +frame 3900 drawable 4320x2160 +frame 4200 drawable 4320x2160 +frame 4500 drawable 4320x2160 +frame 4800 drawable 4320x2160 +7908.515422 health display=2 submitted=5029 presented=5019 skipped=7 fps=89.99 presentAge=0.01s GPUms=6.60 GPUerrors=0 IMU=48060 lastIMU=0.0s status=1 +frame 5100 drawable 4320x2160 +frame 5400 drawable 4320x2160 +frame 5700 drawable 4320x2160 +frame 6000 drawable 4320x2160 +frame 6300 drawable 4320x2160 +frame 6600 drawable 4320x2160 +frame 6900 drawable 4320x2160 +frame 7200 drawable 4320x2160 +frame 7500 drawable 4320x2160 +frame 7800 drawable 4320x2160 +7942.632422 health display=2 submitted=8099 presented=8087 skipped=8 fps=89.93 presentAge=0.01s GPUms=6.60 GPUerrors=0 IMU=51016 lastIMU=0.0s status=1 +frame 8100 drawable 4320x2160 +frame 8400 drawable 4320x2160 +frame 8700 drawable 4320x2160 +7952.002422 health display=2 submitted=8943 presented=8931 skipped=8 fps=90.07 presentAge=0.00s GPUms=6.60 GPUerrors=0 IMU=60388 lastIMU=0.0s status=1 +frame 9000 drawable 4320x2160 +frame 9300 drawable 4320x2160 +frame 9600 drawable 4320x2160 +7962.002422 health display=2 submitted=9843 presented=9831 skipped=8 fps=90.00 presentAge=0.00s GPUms=6.60 GPUerrors=0 IMU=70392 lastIMU=0.0s status=1 +frame 9900 drawable 4320x2160 +frame 10200 drawable 4320x2160 +frame 10500 drawable 4320x2160 +7972.002422 health display=2 submitted=10743 presented=10731 skipped=8 fps=90.00 presentAge=0.00s GPUms=6.60 GPUerrors=0 IMU=80396 lastIMU=0.0s status=1 +frame 10800 drawable 4320x2160 +frame 11100 drawable 4320x2160 +frame 11400 drawable 4320x2160 +7982.001422 health display=2 submitted=11643 presented=11631 skipped=8 fps=90.01 presentAge=0.00s GPUms=6.60 GPUerrors=0 IMU=90396 lastIMU=0.0s status=1 +frame 11700 drawable 4320x2160 +frame 12000 drawable 4320x2160 +frame 12300 drawable 4320x2160 +7992.002422 health display=2 submitted=12543 presented=12531 skipped=8 fps=89.99 presentAge=0.00s GPUms=6.60 GPUerrors=0 IMU=100400 lastIMU=0.0s status=1 +frame 12600 drawable 4320x2160 +frame 12900 drawable 4320x2160 +frame 13200 drawable 4320x2160 +8002.001422 health display=2 submitted=13443 presented=13431 skipped=8 fps=90.00 presentAge=0.00s GPUms=6.59 GPUerrors=0 IMU=110404 lastIMU=0.0s status=1 +frame 13500 drawable 4320x2160 +frame 13800 drawable 4320x2160 +frame 14100 drawable 4320x2160 +8012.001422 health display=2 submitted=14343 presented=14331 skipped=8 fps=90.00 presentAge=0.00s GPUms=6.59 GPUerrors=0 IMU=120404 lastIMU=0.0s status=1 +frame 14400 drawable 4320x2160 +frame 14700 drawable 4320x2160 +frame 15000 drawable 4320x2160 +8022.000422 health display=2 submitted=15243 presented=15231 skipped=8 fps=90.01 presentAge=0.00s GPUms=6.59 GPUerrors=0 IMU=130408 lastIMU=0.0s status=1 +frame 15300 drawable 4320x2160 +frame 15600 drawable 4320x2160 +frame 15900 drawable 4320x2160 +8032.000422 health display=2 submitted=16143 presented=16131 skipped=8 fps=90.00 presentAge=0.00s GPUms=6.59 GPUerrors=0 IMU=140412 lastIMU=0.0s status=1 +frame 16200 drawable 4320x2160 +frame 16500 drawable 4320x2160 +frame 16800 drawable 4320x2160 +8042.000422 health display=2 submitted=17043 presented=17031 skipped=8 fps=90.00 presentAge=0.01s GPUms=6.59 GPUerrors=0 IMU=150412 lastIMU=0.0s status=1 +frame 17100 drawable 4320x2160 +frame 17400 drawable 4320x2160 +frame 17700 drawable 4320x2160 diff --git a/evidence/optics-user-confirmed-checkpoint.json b/evidence/optics-user-confirmed-checkpoint.json @@ -0,0 +1,5 @@ +{ + "attribution": "Unknown; current runtime includes per-axis blue correction and pixel-filtered floor grid. No renderer changes between immediate user reports.", + "visual_validation": "Optical tuning controls were exercised during hardware testing.", + "privacy_note": "Personal optical adjustments and their fingerprint are omitted. Runtime defaults remain factory calibration." +}