4f15ba85
Add partial instance buffer uploads
a73x 2026-04-08 19:29
Commit message
src/renderer.zig
| Old | New | ||
|---|---|---|---|
| @@ -330,6 +330,11 @@ const InstanceUploadDecision = struct { | |||
| 330 | upload_mode: InstanceUploadMode, | 330 | upload_mode: InstanceUploadMode, |
| 331 | }; | 331 | }; |
| 332 | 332 | ||
| 333 | const InstanceRangeWrite = struct { | ||
| 334 | byte_offset: vk.DeviceSize, | ||
| 335 | byte_len: vk.DeviceSize, | ||
| 336 | }; | ||
| 337 | |||
| 333 | fn planInstanceUpload(req: InstanceUploadRequest) InstanceUploadDecision { | 338 | fn planInstanceUpload(req: InstanceUploadRequest) InstanceUploadDecision { |
| 334 | const needed_capacity = std.math.add(u32, req.offset_instances, req.write_len) catch { | 339 | const needed_capacity = std.math.add(u32, req.offset_instances, req.write_len) catch { |
| 335 | return .{ | 340 | return .{ |
| @@ -343,6 +348,32 @@ fn planInstanceUpload(req: InstanceUploadRequest) InstanceUploadDecision { | |||
| 343 | }; | 348 | }; |
| 344 | } | 349 | } |
| 345 | 350 | ||
| 351 | fn planInstanceRangeWrite(offset_instances: u32, len_instances: u32) InstanceRangeWrite { | ||
| 352 | return .{ | ||
| 353 | .byte_offset = @as(vk.DeviceSize, offset_instances) * @sizeOf(Instance), | ||
| 354 | .byte_len = @as(vk.DeviceSize, len_instances) * @sizeOf(Instance), | ||
| 355 | }; | ||
| 356 | } | ||
| 357 | |||
| 358 | fn writeInstanceRange( | ||
| 359 | target: []Instance, | ||
| 360 | offset_instances: u32, | ||
| 361 | instances: []const Instance, | ||
| 362 | ) !void { | ||
| 363 | const decision = planInstanceUpload(.{ | ||
| 364 | .current_capacity = std.math.cast(u32, target.len) orelse return error.InvalidInstanceRange, | ||
| 365 | .offset_instances = offset_instances, | ||
| 366 | .write_len = std.math.cast(u32, instances.len) orelse return error.InvalidInstanceRange, | ||
| 367 | }); | ||
| 368 | switch (decision.upload_mode) { | ||
| 369 | .invalid_range, .full => return error.InvalidInstanceRange, | ||
| 370 | .partial => {}, | ||
| 371 | } | ||
| 372 | |||
| 373 | const offset: usize = @intCast(offset_instances); | ||
| 374 | @memcpy(target[offset .. offset + instances.len], instances); | ||
| 375 | } | ||
| 376 | |||
| 346 | fn swapchainNeedsRebuild(result: vk.Result) bool { | 377 | fn swapchainNeedsRebuild(result: vk.Result) bool { |
| 347 | return result == .suboptimal_khr; | 378 | return result == .suboptimal_khr; |
| 348 | } | 379 | } |
| @@ -1223,6 +1254,39 @@ pub const Context = struct { | |||
| 1223 | self.vkd.unmapMemory(self.device, self.instance_memory); | 1254 | self.vkd.unmapMemory(self.device, self.instance_memory); |
| 1224 | } | 1255 | } |
| 1225 | 1256 | ||
| 1257 | /// Upload a contiguous instance subrange when the existing buffer is large enough. | ||
| 1258 | /// Returns true when the caller must fall back to a full upload instead. | ||
| 1259 | pub fn uploadInstanceRange( | ||
| 1260 | self: *Context, | ||
| 1261 | offset_instances: u32, | ||
| 1262 | instances: []const Instance, | ||
| 1263 | ) !bool { | ||
| 1264 | const decision = planInstanceUpload(.{ | ||
| 1265 | .current_capacity = self.instance_capacity, | ||
| 1266 | .offset_instances = offset_instances, | ||
| 1267 | .write_len = std.math.cast(u32, instances.len) orelse return error.InvalidInstanceRange, | ||
| 1268 | }); | ||
| 1269 | switch (decision.upload_mode) { | ||
| 1270 | .invalid_range => return error.InvalidInstanceRange, | ||
| 1271 | .full => return true, | ||
| 1272 | .partial => {}, | ||
| 1273 | } | ||
| 1274 | |||
| 1275 | if (instances.len == 0) return false; | ||
| 1276 | |||
| 1277 | const range = planInstanceRangeWrite(offset_instances, @intCast(instances.len)); | ||
| 1278 | const mapped = try self.vkd.mapMemory( | ||
| 1279 | self.device, | ||
| 1280 | self.instance_memory, | ||
| 1281 | range.byte_offset, | ||
| 1282 | range.byte_len, | ||
| 1283 | .{}, | ||
| 1284 | ); | ||
| 1285 | @memcpy(@as([*]Instance, @ptrCast(@alignCast(mapped)))[0..instances.len], instances); | ||
| 1286 | self.vkd.unmapMemory(self.device, self.instance_memory); | ||
| 1287 | return false; | ||
| 1288 | } | ||
| 1289 | |||
| 1226 | /// Full draw pass: bind pipeline, push constants, vertex + instance buffers, draw, present. | 1290 | /// Full draw pass: bind pipeline, push constants, vertex + instance buffers, draw, present. |
| 1227 | pub fn drawCells( | 1291 | pub fn drawCells( |
| 1228 | self: *Context, | 1292 | self: *Context, |
| @@ -1430,3 +1494,56 @@ test "range upload reports overflow explicitly" { | |||
| 1430 | try std.testing.expectEqual(@as(?u32, null), decision.needed_capacity); | 1494 | try std.testing.expectEqual(@as(?u32, null), decision.needed_capacity); |
| 1431 | try std.testing.expectEqual(InstanceUploadMode.invalid_range, decision.upload_mode); | 1495 | try std.testing.expectEqual(InstanceUploadMode.invalid_range, decision.upload_mode); |
| 1432 | } | 1496 | } |
| 1497 | |||
| 1498 | fn testInstance(seed: f32) Instance { | ||
| 1499 | return .{ | ||
| 1500 | .cell_pos = .{ seed, seed + 1.0 }, | ||
| 1501 | .glyph_size = .{ seed + 2.0, seed + 3.0 }, | ||
| 1502 | .glyph_bearing = .{ seed + 4.0, seed + 5.0 }, | ||
| 1503 | .uv_rect = .{ seed + 6.0, seed + 7.0, seed + 8.0, seed + 9.0 }, | ||
| 1504 | .fg = .{ seed + 10.0, seed + 11.0, seed + 12.0, seed + 13.0 }, | ||
| 1505 | .bg = .{ seed + 14.0, seed + 15.0, seed + 16.0, seed + 17.0 }, | ||
| 1506 | }; | ||
| 1507 | } | ||
| 1508 | |||
| 1509 | test "uploadInstanceRangeWrite computes byte offset from instance offset" { | ||
| 1510 | const write = planInstanceRangeWrite(3, 2); | ||
| 1511 | try std.testing.expectEqual(@as(vk.DeviceSize, 3 * @sizeOf(Instance)), write.byte_offset); | ||
| 1512 | try std.testing.expectEqual(@as(vk.DeviceSize, 2 * @sizeOf(Instance)), write.byte_len); | ||
| 1513 | } | ||
| 1514 | |||
| 1515 | test "writeInstanceRange overwrites only the requested window" { | ||
| 1516 | var target = [_]Instance{ | ||
| 1517 | testInstance(0), | ||
| 1518 | testInstance(20), | ||
| 1519 | testInstance(40), | ||
| 1520 | testInstance(60), | ||
| 1521 | }; | ||
| 1522 | const replacement = [_]Instance{ | ||
| 1523 | testInstance(100), | ||
| 1524 | testInstance(120), | ||
| 1525 | }; | ||
| 1526 | |||
| 1527 | try writeInstanceRange(target[0..], 1, replacement[0..]); | ||
| 1528 | |||
| 1529 | try std.testing.expectEqualDeep(testInstance(0), target[0]); | ||
| 1530 | try std.testing.expectEqualDeep(testInstance(100), target[1]); | ||
| 1531 | try std.testing.expectEqualDeep(testInstance(120), target[2]); | ||
| 1532 | try std.testing.expectEqualDeep(testInstance(60), target[3]); | ||
| 1533 | } | ||
| 1534 | |||
| 1535 | test "writeInstanceRange rejects writes past the backing slice" { | ||
| 1536 | var target = [_]Instance{ | ||
| 1537 | testInstance(0), | ||
| 1538 | testInstance(20), | ||
| 1539 | }; | ||
| 1540 | const replacement = [_]Instance{ | ||
| 1541 | testInstance(100), | ||
| 1542 | testInstance(120), | ||
| 1543 | }; | ||
| 1544 | |||
| 1545 | try std.testing.expectError( | ||
| 1546 | error.InvalidInstanceRange, | ||
| 1547 | writeInstanceRange(target[0..], 1, replacement[0..]), | ||
| 1548 | ); | ||
| 1549 | } | ||