Skip to content

Commit 87b376c

Browse files
visual bugfix for cars, metal performance improvements
1 parent a00948c commit 87b376c

2 files changed

Lines changed: 162 additions & 20 deletions

File tree

Sources/SumoGUIMac/Render/NetworkView.swift

Lines changed: 114 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ final class NetworkMetalView: NSView, MTKViewDelegate {
111111
private var lastLaneLODScale: Float?
112112
private var vehicleInstanceBuffer: MTLBuffer?
113113
private var vehicleInstanceCount = 0
114+
private var vehicleInstanceCapacity = 0
114115
private var lastVehicleLODScale: Float?
115116
private var lastDragLocation: CGPoint?
116117
private var mouseDownLocation: CGPoint?
@@ -524,7 +525,7 @@ final class NetworkMetalView: NSView, MTKViewDelegate {
524525
encoder.setRenderPipelineState(pipeline)
525526
encoder.setVertexBuffer(vehicleInstanceBuffer, offset: 0, index: 0)
526527
encoder.setVertexBytes(&uniforms, length: MemoryLayout<LaneViewportUniforms>.stride, index: 1)
527-
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount: vehicleInstanceCount)
528+
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 9, instanceCount: vehicleInstanceCount)
528529
}
529530

530531
encoder.endEncoding()
@@ -1141,8 +1142,15 @@ final class NetworkMetalView: NSView, MTKViewDelegate {
11411142
private func updateVehicleBuffer(samples: [VehicleRenderSample], scale: Float? = nil) {
11421143
let renderScale = scale ?? currentRenderScale()
11431144
lastVehicleLODScale = renderScale
1144-
guard let device, samples.isEmpty == false else {
1145+
guard let device else {
11451146
vehicleInstanceBuffer = nil
1147+
vehicleInstanceCount = 0
1148+
vehicleInstanceCapacity = 0
1149+
metalView.setNeedsDisplay(bounds)
1150+
return
1151+
}
1152+
1153+
guard samples.isEmpty == false, let size = RenderLOD.vehicleScreenSize(scale: renderScale) else {
11461154
vehicleInstanceCount = 0
11471155
metalView.setNeedsDisplay(bounds)
11481156
return
@@ -1151,9 +1159,6 @@ final class NetworkMetalView: NSView, MTKViewDelegate {
11511159
var instances: [VehicleInstance] = []
11521160
instances.reserveCapacity(samples.count)
11531161
for vehicle in samples {
1154-
guard let size = RenderLOD.vehicleScreenSize(scale: renderScale) else {
1155-
continue
1156-
}
11571162
let isSelected = vehicle.id == selectedVehicleID || selectedVehicleIDs.contains(vehicle.id)
11581163
let color = isSelected ? selectedVehicleColor() : vehicleColor(for: vehicle)
11591164
instances.append(VehicleInstance(
@@ -1164,12 +1169,25 @@ final class NetworkMetalView: NSView, MTKViewDelegate {
11641169
}
11651170
vehicleInstanceCount = instances.count
11661171
guard instances.isEmpty == false else {
1167-
vehicleInstanceBuffer = nil
11681172
metalView.setNeedsDisplay(bounds)
11691173
return
11701174
}
1171-
vehicleInstanceBuffer = instances.withUnsafeBytes { bytes in
1172-
device.makeBuffer(bytes: bytes.baseAddress!, length: bytes.count, options: .storageModeShared)
1175+
1176+
if vehicleInstanceBuffer == nil || vehicleInstanceCapacity < instances.count {
1177+
let nextCapacity = max(instances.count, max(vehicleInstanceCapacity * 2, 256))
1178+
vehicleInstanceBuffer = device.makeBuffer(
1179+
length: nextCapacity * MemoryLayout<VehicleInstance>.stride,
1180+
options: .storageModeShared
1181+
)
1182+
vehicleInstanceCapacity = nextCapacity
1183+
}
1184+
1185+
if let vehicleInstanceBuffer {
1186+
instances.withUnsafeBytes { bytes in
1187+
if let baseAddress = bytes.baseAddress {
1188+
vehicleInstanceBuffer.contents().copyMemory(from: baseAddress, byteCount: bytes.count)
1189+
}
1190+
}
11731191
}
11741192
metalView.setNeedsDisplay(bounds)
11751193
}
@@ -1419,6 +1437,56 @@ enum RenderLOD {
14191437
}
14201438
}
14211439

1440+
enum VehicleHeading {
1441+
static func screenRadians(sumoDegrees: Float, viewportRotationRadians: Float) -> Float {
1442+
((sumoDegrees - 90) * .pi / 180) - viewportRotationRadians
1443+
}
1444+
1445+
static func sumoDegrees(from source: SIMD2<Float>, to target: SIMD2<Float>) -> Float? {
1446+
let delta = target - source
1447+
guard simd_length_squared(delta) > 0.0001 else { return nil }
1448+
return normalizedDegrees(atan2(delta.x, delta.y) * 180 / .pi)
1449+
}
1450+
1451+
static func interpolatedSUMODegrees(
1452+
from sourceAngle: Float,
1453+
to targetAngle: Float,
1454+
sourcePosition: SIMD2<Float>,
1455+
targetPosition: SIMD2<Float>,
1456+
progress: Float
1457+
) -> Float {
1458+
let clamped = max(0, min(progress, 1))
1459+
let baseAngle = mixDegrees(sourceAngle, targetAngle, progress: clamped)
1460+
guard let movementAngle = sumoDegrees(from: sourcePosition, to: targetPosition) else {
1461+
return baseAngle
1462+
}
1463+
1464+
let midTurnWeight = sin(clamped * .pi)
1465+
return mixDegrees(baseAngle, movementAngle, progress: min(midTurnWeight * 0.65, 0.65))
1466+
}
1467+
1468+
static func mixDegrees(_ source: Float, _ target: Float, progress: Float) -> Float {
1469+
let clamped = max(0, min(progress, 1))
1470+
return normalizedDegrees(source + shortestDeltaDegrees(from: source, to: target) * clamped)
1471+
}
1472+
1473+
static func shortestDeltaDegrees(from source: Float, to target: Float) -> Float {
1474+
var delta = normalizedDegrees(target) - normalizedDegrees(source)
1475+
while delta > 180 { delta -= 360 }
1476+
while delta < -180 { delta += 360 }
1477+
return delta
1478+
}
1479+
1480+
private static func normalizedDegrees(_ degrees: Float) -> Float {
1481+
guard degrees.isFinite else { return 0 }
1482+
var normalized = degrees.truncatingRemainder(dividingBy: 360)
1483+
if normalized < 0 {
1484+
normalized += 360
1485+
}
1486+
return normalized
1487+
}
1488+
}
1489+
14221490
private func screenDistance(from a: CGPoint, to b: CGPoint) -> CGFloat {
14231491
let dx = a.x - b.x
14241492
let dy = a.y - b.y
@@ -1486,13 +1554,16 @@ private struct VehicleRenderSample {
14861554

14871555
func interpolated(to target: VehicleRenderSample, progress: Float) -> VehicleRenderSample {
14881556
let clamped = max(0, min(progress, 1))
1489-
var angleDelta = target.angle - angle
1490-
while angleDelta > 180 { angleDelta -= 360 }
1491-
while angleDelta < -180 { angleDelta += 360 }
14921557
return VehicleRenderSample(
14931558
id: target.id,
14941559
position: position + (target.position - position) * clamped,
1495-
angle: angle + angleDelta * clamped,
1560+
angle: VehicleHeading.interpolatedSUMODegrees(
1561+
from: angle,
1562+
to: target.angle,
1563+
sourcePosition: position,
1564+
targetPosition: target.position,
1565+
progress: clamped
1566+
),
14961567
speed: speed + (target.speed - speed) * clamped,
14971568
typeID: target.typeID,
14981569
acceleration: interpolateOptional(acceleration, target.acceleration, progress: clamped),
@@ -1694,15 +1765,15 @@ vertex BackgroundVertexOut backgroundVertex(
16941765
const device BackgroundRenderVertex *vertices [[buffer(0)]],
16951766
constant LaneViewportUniforms &uniforms [[buffer(1)]]
16961767
) {
1697-
const BackgroundRenderVertex vertex = vertices[vertexID];
1698-
const float2 world = float2(vertex.position);
1768+
const BackgroundRenderVertex backgroundVertexData = vertices[vertexID];
1769+
const float2 world = float2(backgroundVertexData.position);
16991770
const float2 size = uniforms.viewport.xy;
17001771
const float2 screen = worldToScreen(world, uniforms);
17011772
17021773
BackgroundVertexOut out;
17031774
out.position = screenToClip(screen, size);
1704-
out.texCoord = float2(vertex.texCoord);
1705-
out.tint = vertex.tint;
1775+
out.texCoord = float2(backgroundVertexData.texCoord);
1776+
out.tint = backgroundVertexData.tint;
17061777
return out;
17071778
}
17081779
@@ -1798,23 +1869,46 @@ vertex VehicleVertexOut vehicleVertex(
17981869
const VehicleInstance vehicle = vehicles[instanceID];
17991870
const float2 size = uniforms.viewport.xy;
18001871
const float2 world = vehicle.pose.xy;
1801-
const float angle = (90.0 - vehicle.pose.z) * 0.01745329252 + uniforms.camera.w;
1872+
const float angle = (vehicle.pose.z - 90.0) * 0.01745329252 - uniforms.camera.w;
18021873
const float2 forward = float2(cos(angle), sin(angle));
18031874
const float2 right = float2(-forward.y, forward.x);
18041875
const float length = clamp(vehicle.metrics.x, 2.0, 34.0);
18051876
const float width = clamp(vehicle.metrics.y, 1.0, 14.0);
18061877
const float2 screenCenter = worldToScreen(world, uniforms);
1878+
const float2 nose = screenCenter + forward * (length * 0.58);
1879+
const float2 frontRight = screenCenter + forward * (length * 0.14) + right * (width * 0.52);
1880+
const float2 rearRight = screenCenter - forward * (length * 0.44) + right * (width * 0.42);
1881+
const float2 rearLeft = screenCenter - forward * (length * 0.44) - right * (width * 0.42);
1882+
const float2 frontLeft = screenCenter + forward * (length * 0.14) - right * (width * 0.52);
18071883
18081884
float2 screen;
18091885
switch (vertexID) {
18101886
case 0:
1811-
screen = screenCenter + forward * (length * 0.58);
1887+
screen = nose;
18121888
break;
18131889
case 1:
1814-
screen = screenCenter - forward * (length * 0.42) - right * (width * 0.5);
1890+
screen = frontRight;
1891+
break;
1892+
case 2:
1893+
screen = frontLeft;
1894+
break;
1895+
case 3:
1896+
screen = frontRight;
1897+
break;
1898+
case 4:
1899+
screen = rearRight;
1900+
break;
1901+
case 5:
1902+
screen = rearLeft;
1903+
break;
1904+
case 6:
1905+
screen = frontRight;
1906+
break;
1907+
case 7:
1908+
screen = rearLeft;
18151909
break;
18161910
default:
1817-
screen = screenCenter - forward * (length * 0.42) + right * (width * 0.5);
1911+
screen = frontLeft;
18181912
break;
18191913
}
18201914

Tests/SumoGUIMacTests/NetworkRenderLODTests.swift

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,52 @@ final class NetworkRenderLODTests: XCTestCase {
2020
XCTAssertEqual(zoomedSize?.x ?? 0, 10, accuracy: 0.001)
2121
XCTAssertEqual(zoomedSize?.y ?? 0, 4, accuracy: 0.001)
2222
}
23+
24+
func testSUMOHeadingConvertsToScreenCoordinates() {
25+
XCTAssertEqual(VehicleHeading.screenRadians(sumoDegrees: 0, viewportRotationRadians: 0), -.pi / 2, accuracy: 0.001)
26+
XCTAssertEqual(VehicleHeading.screenRadians(sumoDegrees: 90, viewportRotationRadians: 0), 0, accuracy: 0.001)
27+
XCTAssertEqual(VehicleHeading.screenRadians(sumoDegrees: 180, viewportRotationRadians: 0), .pi / 2, accuracy: 0.001)
28+
XCTAssertEqual(
29+
VehicleHeading.screenRadians(sumoDegrees: 90, viewportRotationRadians: .pi / 2),
30+
-.pi / 2,
31+
accuracy: 0.001
32+
)
33+
}
34+
35+
func testMovementHeadingUsesSUMOAngleConvention() {
36+
XCTAssertEqual(
37+
VehicleHeading.sumoDegrees(from: SIMD2<Float>(0, 0), to: SIMD2<Float>(0, 10)) ?? -1,
38+
0,
39+
accuracy: 0.001
40+
)
41+
XCTAssertEqual(
42+
VehicleHeading.sumoDegrees(from: SIMD2<Float>(0, 0), to: SIMD2<Float>(10, 0)) ?? -1,
43+
90,
44+
accuracy: 0.001
45+
)
46+
XCTAssertEqual(
47+
VehicleHeading.sumoDegrees(from: SIMD2<Float>(0, 0), to: SIMD2<Float>(0, -10)) ?? -1,
48+
180,
49+
accuracy: 0.001
50+
)
51+
XCTAssertEqual(
52+
VehicleHeading.sumoDegrees(from: SIMD2<Float>(0, 0), to: SIMD2<Float>(-10, 0)) ?? -1,
53+
270,
54+
accuracy: 0.001
55+
)
56+
}
57+
58+
func testHeadingInterpolationUsesShortestTurnAndMovementBias() {
59+
XCTAssertEqual(VehicleHeading.mixDegrees(350, 10, progress: 0.5), 0, accuracy: 0.001)
60+
61+
let biased = VehicleHeading.interpolatedSUMODegrees(
62+
from: 0,
63+
to: 90,
64+
sourcePosition: SIMD2<Float>(0, 0),
65+
targetPosition: SIMD2<Float>(10, 0),
66+
progress: 0.5
67+
)
68+
XCTAssertGreaterThan(biased, 45)
69+
XCTAssertLessThan(biased, 90)
70+
}
2371
}

0 commit comments

Comments
 (0)