From e62b96ae62284fa1873176d4a2aec2cb2ca52b47 Mon Sep 17 00:00:00 2001 From: Micah Alpern Date: Wed, 29 Jul 2026 09:42:26 -0700 Subject: [PATCH] Make wear and jitter knobs honest about their ranges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review findings on the tuning surface: - The shader saturated wearStrength, so the shipped 1.5 default silently behaved as 1.0 and overrides above 1.0 were no-ops. Drop the clamp (plain multiplier like its siblings) and set the default to 1.0 — the same effective value the goldens and installed review approved. - Seed-only gloss variation (roughness offset, specular gain) ignored highlightJitter, so zeroing both knobs could not restore the uniform baseline. Gate both terms with min(highlightJitter, 1): identical at the shipped default, fully off at zero. - Cover all gpuVectorE and vignetteVector lanes in the packing test. Metal goldens compare unchanged, confirming no visual drift at the shipped defaults. Co-Authored-By: Claude Fable 5 --- .../UI/KeyboardStage/Metal/KeyboardStage.metal | 12 +++++++++--- .../UI/KeyboardStage/Metal/KeyboardStageTuning.swift | 3 ++- .../KeyboardStage/KeyboardStageTuningTests.swift | 6 ++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Sources/KeyPathAppKit/UI/KeyboardStage/Metal/KeyboardStage.metal b/Sources/KeyPathAppKit/UI/KeyboardStage/Metal/KeyboardStage.metal index 578ab7a6b..78935a528 100644 --- a/Sources/KeyPathAppKit/UI/KeyboardStage/Metal/KeyboardStage.metal +++ b/Sources/KeyPathAppKit/UI/KeyboardStage/Metal/KeyboardStage.metal @@ -331,7 +331,7 @@ fragment float4 keypath_keyboard_stage_fragment( // drive gloss, grain, and highlight variation; both derive from the key // code, so the pattern never shifts between moments or launches. float wear = isKey - ? saturate(input.material.x) * saturate(uniforms.tuningE.z) + ? saturate(input.material.x) * max(uniforms.tuningE.z, 0.0) : 0.0; float materialSeedA = keypath_hash(float2(input.material.y, 0.37)); float materialSeedB = keypath_hash(float2(input.material.y, 0.71)); @@ -565,7 +565,9 @@ fragment float4 keypath_keyboard_stage_fragment( (isDeck ? 0.31 : 0.47) + roughnessVariation * (isDeck ? 0.055 : 0.035) - wear * 0.11 - + (isKey ? (materialSeedA - 0.5) * 0.05 : 0.0), + + (isKey + ? (materialSeedA - 0.5) * 0.05 * min(uniforms.tuningE.w, 1.0) + : 0.0), 0.12, 0.82 ); @@ -584,8 +586,12 @@ fragment float4 keypath_keyboard_stage_fragment( * microfacetVisibility / max(0.04, 4.0 * normalDotView * normalDotLight); float specularPower = isDeck ? 88.0 : (isKey ? 42.0 : 34.0); + // Seed-only gloss deviation collapses to 1.0 as highlightJitter + // approaches zero, so the tuning knobs can fully restore the uniform + // pre-wear material. float keySpecularGain = isKey - ? (0.85 + materialSeedB * 0.30) * (1.0 + wear * 0.55) + ? (1.0 + (materialSeedB - 0.5) * 0.30 * min(uniforms.tuningE.w, 1.0)) + * (1.0 + wear * 0.55) : 1.0; float specular = pow(saturate(dot(normal, halfVector)), specularPower) * keySpecularGain; diff --git a/Sources/KeyPathAppKit/UI/KeyboardStage/Metal/KeyboardStageTuning.swift b/Sources/KeyPathAppKit/UI/KeyboardStage/Metal/KeyboardStageTuning.swift index b14dc65d0..398795d14 100644 --- a/Sources/KeyPathAppKit/UI/KeyboardStage/Metal/KeyboardStageTuning.swift +++ b/Sources/KeyPathAppKit/UI/KeyboardStage/Metal/KeyboardStageTuning.swift @@ -49,7 +49,8 @@ struct KeyboardStageTuning: Equatable, Sendable { var fillLightStrength: Float = 0.95 var fillSpecularStrength: Float = 0.15 /// Per-key wear (polished crowns, calmer grain on high-traffic caps). - var wearStrength: Float = 1.5 + /// A plain multiplier: values above 1 amplify the per-key wear levels. + var wearStrength: Float = 1.0 /// Per-key highlight jitter (each cap catches the light slightly apart). var highlightJitter: Float = 2.2 /// Window-space corner falloff; relaxes to 30% at the settled light. diff --git a/Tests/KeyPathTests/KeyboardStage/KeyboardStageTuningTests.swift b/Tests/KeyPathTests/KeyboardStage/KeyboardStageTuningTests.swift index 595050dad..c942a29e6 100644 --- a/Tests/KeyPathTests/KeyboardStage/KeyboardStageTuningTests.swift +++ b/Tests/KeyPathTests/KeyboardStage/KeyboardStageTuningTests.swift @@ -32,6 +32,12 @@ final class KeyboardStageTuningTests: XCTestCase { XCTAssertEqual(tuning.gpuVectorC.w, tuning.backlightStrengthDark) XCTAssertEqual(tuning.gpuVectorD.y, tuning.legendEmissionMax) XCTAssertEqual(tuning.gpuVectorE.x, tuning.fillLightStrength) + XCTAssertEqual(tuning.gpuVectorE.y, tuning.fillSpecularStrength) + XCTAssertEqual(tuning.gpuVectorE.z, tuning.wearStrength) + XCTAssertEqual(tuning.gpuVectorE.w, tuning.highlightJitter) + XCTAssertEqual(tuning.vignetteVector.x, tuning.vignetteStrength) + XCTAssertEqual(tuning.vignetteVector.y, tuning.vignetteInnerRadius) + XCTAssertEqual(tuning.gpuVectorE.x, tuning.fillLightStrength) XCTAssertEqual(tuning.gpuVectorE.z, tuning.wearStrength) }