a73x

4f15ba85

Add partial instance buffer uploads

a73x   2026-04-08 19:29

Commit message
Add partial instance buffer uploads

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 }