tiny.sdfii.internals.command.sink
Defined in internals.command.
API (25)
Actions
Public operations.
Types and contracts
Public types and contracts.
ApplyResultBatchResultCommandCommandKindEntityIdErrorEventResultEventSpecMutationResultPhysicsBlobSpecSpawnManyResultSpawnResultSpawnSpecTransformDescTransformSpec
Values and defaults
Public values and defaults.
Source
Source: fun/sdfii/src/engine/command/root.zig:1
zig
pub const sink = @import("sink.zig");Source: fun/sdfii/src/engine/command/sink.zig
zig
const std = @import("std");const ecs = @import("sdfii_ecs");const sdfii_world = @import("sdfii_world");const WorldStore = sdfii_world.store;const Commands = sdfii_world.commands;const Schema = sdfii_world.schema;const Self = @This();pub const EntityId = ecs.EntityId;pub const TransformDesc = ecs.SdfiiTransformDesc;pub const physics_component_schema_id: u64 = @backingInt(Schema.ComponentSchema.physics);pub const SpawnSpec = struct { name: ?[]const u8 = null, key: ?[]const u8 = null, transform: ?TransformDesc = null,};pub const PhysicsBlobSpec = struct { entity: EntityId, payload: []const u8,};pub const TransformSpec = struct { entity: EntityId, transform: TransformDesc,};pub const EventSpec = struct { schema_id: u64, payload: []const u8 = &.{},};pub const Command = union(enum) { spawn: SpawnSpec, despawn: EntityId, attach_physics_blob: PhysicsBlobSpec, set_transform: TransformSpec, emit: EventSpec,};pub const CommandKind = enum(u8) { spawn = 1, despawn = 2, attach_physics = 3, set_transform = 4, emit = 5,};pub const Error = error{ BatchTooLarge, InvalidUtf8, SpawnBufferTooSmall, SubmitFailed, OutOfMemory,};pub const BatchResult = struct { command_count: u32, spawned_count: u32,};pub const SpawnResult = struct { entity: EntityId, command_count: u32,};pub const SpawnManyResult = struct { count: usize, command_count: u32,};pub const MutationResult = struct { command_count: u32,};pub const EventResult = struct { command_count: u32, visible_event_count: usize,};pub const ApplyResult = struct { command_count: usize, spawned_count: usize, event_count: usize, visible_event_count: usize,};allocator: std.mem.Allocator,world: *WorldStore,pub fn bind(allocator: std.mem.Allocator, world: *WorldStore) Self { return .{ .allocator = allocator, .world = world, };}pub fn submitBatch(self: *const Self, batch: *Commands.Batch, spawned_ids: ?[]EntityId) Error!BatchResult { const command_count = batch.count(); const spawned_count = self.world.submit(batch, spawned_ids) catch |err| return mapError(err); return .{ .command_count = command_count, .spawned_count = spawned_count, };}pub fn spawn(self: *const Self, spec: SpawnSpec) Error!SpawnResult { var batch = try Commands.Batch.init(self.allocator, 4); defer batch.deinit(); try batch.pushSpawn(spec.name, spec.key, spec.transform); var spawned: [1]EntityId = .{.{ .index = std.math.maxInt(u32), .generation = 0 }}; const result = try self.submitBatch(&batch, spawned[0..]); std.debug.assert(result.spawned_count == 1); return .{ .entity = spawned[0], .command_count = result.command_count, };}pub fn spawnMany(self: *const Self, specs: []const SpawnSpec, out: []EntityId) Error!SpawnManyResult { if (out.len < specs.len) return error.SpawnBufferTooSmall; if (specs.len == 0) return .{ .count = 0, .command_count = 0 }; const capacity_hint: u32 = std.math.cast(u32, specs.len) orelse return error.BatchTooLarge; var batch = try Commands.Batch.init(self.allocator, @max(capacity_hint, @as(u32, 4))); defer batch.deinit(); for (specs) |spec| { try batch.pushSpawn(spec.name, spec.key, spec.transform); } const result = try self.submitBatch(&batch, out[0..specs.len]); const spawned_count: usize = @intCast(result.spawned_count); std.debug.assert(spawned_count == specs.len); return .{ .count = spawned_count, .command_count = result.command_count, };}pub fn despawn(self: *const Self, entity: EntityId) Error!MutationResult { var batch = try Commands.Batch.init(self.allocator, 4); defer batch.deinit(); try batch.pushDespawn(entity); const result = try self.submitBatch(&batch, null); return .{ .command_count = result.command_count };}pub fn attachPhysicsBlob(self: *const Self, entity: EntityId, payload: []const u8) Error!MutationResult { var batch = try Commands.Batch.init(self.allocator, 4); defer batch.deinit(); try batch.pushUpsertComponent(entity, physics_component_schema_id, payload); const result = try self.submitBatch(&batch, null); return .{ .command_count = result.command_count };}pub fn setTransform(self: *const Self, entity: EntityId, transform: TransformDesc) Error!MutationResult { var batch = try Commands.Batch.init(self.allocator, 4); defer batch.deinit(); try batch.pushSetTransform(entity, transform); const result = try self.submitBatch(&batch, null); return .{ .command_count = result.command_count };}pub fn emit(self: *const Self, schema_id: u64, payload: []const u8) Error!EventResult { var batch = try Commands.Batch.init(self.allocator, 4); defer batch.deinit(); try batch.pushEnqueueEvent(schema_id, payload); try batch.pushPublishEvents(); const result = try self.submitBatch(&batch, null); return .{ .command_count = result.command_count, .visible_event_count = self.world.visibleEventCount(), };}pub fn applyCommands( self: *const Self, commands: []const Command, spawned_out: []EntityId,) Error!ApplyResult { var expected_spawn_count: usize = 0; var event_count: usize = 0; for (commands) |command| { switch (command) { .spawn => expected_spawn_count += 1, .emit => event_count += 1, .despawn, .attach_physics_blob, .set_transform => {}, } } if (spawned_out.len < expected_spawn_count) return error.SpawnBufferTooSmall; if (commands.len == 0) return .{ .command_count = 0, .spawned_count = 0, .event_count = 0, .visible_event_count = self.world.visibleEventCount(), }; const internal_count = std.math.add(usize, commands.len, @intFromBool(event_count != 0)) catch return error.BatchTooLarge; const capacity_hint = std.math.cast(u32, internal_count) orelse return error.BatchTooLarge; var batch = try Commands.Batch.init(self.allocator, @max(capacity_hint, @as(u32, 4))); defer batch.deinit(); for (commands) |command| { switch (command) { .spawn => |spec| try batch.pushSpawn(spec.name, spec.key, spec.transform), .despawn => |entity| try batch.pushDespawn(entity), .attach_physics_blob => |spec| try batch.pushUpsertComponent( spec.entity, physics_component_schema_id, spec.payload, ), .set_transform => |spec| try batch.pushSetTransform(spec.entity, spec.transform), .emit => |spec| try batch.pushEnqueueEvent(spec.schema_id, spec.payload), } } if (event_count != 0) try batch.pushPublishEvents(); const result = try self.submitBatch(&batch, spawned_out[0..expected_spawn_count]); const spawned_count: usize = @intCast(result.spawned_count); std.debug.assert(spawned_count == expected_spawn_count); return .{ .command_count = commands.len, .spawned_count = spawned_count, .event_count = event_count, .visible_event_count = self.world.visibleEventCount(), };}fn mapError(err: anyerror) Error { return switch (err) { error.OutOfMemory => error.OutOfMemory, error.InvalidUtf8 => error.InvalidUtf8, error.SpawnBufferTooSmall => error.SpawnBufferTooSmall, error.BatchTooLarge => error.BatchTooLarge, else => error.SubmitFailed, };}test "direct command sink spawns and updates engine-owned storage" { const allocator = std.testing.allocator; var world = try WorldStore.init(allocator, 8); defer world.deinit(); world.markEngineOwned(); const sink = Self.bind(allocator, &world); const spawned = try sink.spawn(.{ .name = "player", .key = "hero", .transform = .{ .pos_x = 1, .pos_y = 2, .pos_z = 3 }, }); try std.testing.expectEqual(@as(u32, 1), spawned.command_count); try std.testing.expect(world.isAlive(spawned.entity)); const updated = try sink.setTransform(spawned.entity, .{ .pos_x = 4, .pos_y = 5, .pos_z = 6 }); try std.testing.expectEqual(@as(u32, 1), updated.command_count); const stored = world.transformDesc(spawned.entity).?; try std.testing.expectEqual(@as(f32, 4), stored.pos_x); try std.testing.expectEqual(@as(f32, 5), stored.pos_y); try std.testing.expectEqual(@as(f32, 6), stored.pos_z);}test "direct command sink preserves batch ordering" { const allocator = std.testing.allocator; var world = try WorldStore.init(allocator, 8); defer world.deinit(); world.markEngineOwned(); const sink = Self.bind(allocator, &world); const specs = [_]SpawnSpec{ .{ .name = "a", .transform = .{ .pos_x = 1 } }, .{ .key = "b" }, .{ .name = "c", .transform = .{ .pos_z = 3 } }, }; var out: [3]EntityId = undefined; const result = try sink.spawnMany(&specs, &out); try std.testing.expectEqual(@as(usize, 3), result.count); try std.testing.expectEqual(@as(u32, 3), result.command_count); for (out) |entity| { try std.testing.expect(world.isAlive(entity)); } try std.testing.expectEqual(@as(f32, 1), world.transformDesc(out[0]).?.pos_x); try std.testing.expect(world.transformDesc(out[1]) == null); try std.testing.expectEqual(@as(f32, 3), world.transformDesc(out[2]).?.pos_z);}test "spawn many rejects a late invalid spec without touching world or output" { const allocator = std.testing.allocator; var world = try WorldStore.init(allocator, 8); defer world.deinit(); world.markEngineOwned(); const sink = Self.bind(allocator, &world); const invalid_utf8 = [_]u8{0xff}; const specs = [_]SpawnSpec{ .{ .name = "valid" }, .{ .name = &invalid_utf8 }, }; const sentinel = EntityId{ .index = std.math.maxInt(u32), .generation = std.math.maxInt(u32) }; var out: [2]EntityId = .{ sentinel, sentinel }; try std.testing.expectError(error.InvalidUtf8, sink.spawnMany(&specs, &out)); try std.testing.expectEqual(@as(u32, 0), world.aliveCount()); try std.testing.expectEqual(sentinel, out[0]); try std.testing.expectEqual(sentinel, out[1]);}test "typed command list reports spawned and emitted commands" { const allocator = std.testing.allocator; var world = try WorldStore.init(allocator, 8); defer world.deinit(); world.markEngineOwned(); const sink = Self.bind(allocator, &world); const commands = [_]Command{ .{ .spawn = .{ .name = "a" } }, .{ .emit = .{ .schema_id = 9, .payload = "evt" } }, }; var spawned: [1]EntityId = undefined; const result = try sink.applyCommands(&commands, &spawned); try std.testing.expectEqual(@as(usize, 2), result.command_count); try std.testing.expectEqual(@as(usize, 1), result.spawned_count); try std.testing.expectEqual(@as(usize, 1), result.event_count); try std.testing.expect(world.isAlive(spawned[0])); try std.testing.expectEqual(@as(usize, 1), world.visibleEventCount());}test "typed command list commits atomically and publishes all emitted events" { const allocator = std.testing.allocator; var world = try WorldStore.init(allocator, 8); defer world.deinit(); world.markEngineOwned(); const sink = Self.bind(allocator, &world); const invalid_utf8 = [_]u8{0xff}; const rejected = [_]Command{ .{ .spawn = .{ .name = "would-commit-first" } }, .{ .spawn = .{ .name = &invalid_utf8 } }, }; const rejected_sentinel = EntityId{ .index = std.math.maxInt(u32), .generation = std.math.maxInt(u32), }; var rejected_spawns: [2]EntityId = .{ rejected_sentinel, rejected_sentinel }; try std.testing.expectError(error.InvalidUtf8, sink.applyCommands(&rejected, &rejected_spawns)); try std.testing.expectEqual(@as(u32, 0), world.aliveCount()); try std.testing.expectEqual(rejected_sentinel, rejected_spawns[0]); try std.testing.expectEqual(rejected_sentinel, rejected_spawns[1]); const accepted = [_]Command{ .{ .emit = .{ .schema_id = 7, .payload = "first" } }, .{ .emit = .{ .schema_id = 8, .payload = "second" } }, }; var no_spawns: [0]EntityId = .{}; const result = try sink.applyCommands(&accepted, &no_spawns); try std.testing.expectEqual(@as(usize, 2), result.event_count); try std.testing.expectEqual(@as(usize, 2), result.visible_event_count); try std.testing.expectEqual(@as(u64, 7), world.visibleEvent(0).?.schema_id); try std.testing.expectEqual(@as(u64, 8), world.visibleEvent(1).?.schema_id);}test "typed command list rolls back every allocation failure and retries" { var saw_operation_failure = false; var completed_sweep = false; for (0..512) |fail_index| { var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = fail_index }); var world = WorldStore.init(failing.allocator(), 2) catch |err| { try std.testing.expectEqual(error.OutOfMemory, err); try std.testing.expectEqual(failing.allocated_bytes, failing.freed_bytes); continue; }; const existing = world.entities.spawn() catch |err| { try std.testing.expectEqual(error.OutOfMemory, err); world.deinit(); try std.testing.expectEqual(failing.allocated_bytes, failing.freed_bytes); continue; }; world.transforms.insert(existing.index, .{ .pos_x = 1 }) catch |err| { try std.testing.expectEqual(error.OutOfMemory, err); world.deinit(); try std.testing.expectEqual(failing.allocated_bytes, failing.freed_bytes); continue; }; world.markEngineOwned(); const sink = Self.bind(failing.allocator(), &world); const commands = [_]Command{ .{ .set_transform = .{ .entity = existing, .transform = .{ .pos_x = 9 } } }, .{ .emit = .{ .schema_id = 42, .payload = "event" } }, .{ .spawn = .{ .name = "spawned", .transform = .{ .pos_z = 3 } } }, }; const sentinel = EntityId{ .index = std.math.maxInt(u32), .generation = std.math.maxInt(u32) }; var spawned: [1]EntityId = .{sentinel}; const attempt = sink.applyCommands(&commands, &spawned); if (attempt) |result| { try std.testing.expect(!failing.has_induced_failure); try expectMixedBatchResult(&world, existing, spawned[0], result); completed_sweep = true; } else |err| { try std.testing.expectEqual(error.OutOfMemory, err); try std.testing.expect(failing.has_induced_failure); saw_operation_failure = true; try std.testing.expectEqual(@as(u32, 1), world.aliveCount()); try std.testing.expectEqual(@as(f32, 1), world.transformDesc(existing).?.pos_x); try std.testing.expectEqual(@as(usize, 0), world.visibleEventCount()); try std.testing.expectEqual(@as(usize, 0), world.pendingEventCount()); try std.testing.expectEqual(sentinel, spawned[0]); failing.fail_index = std.math.maxInt(usize); failing.resize_fail_index = std.math.maxInt(usize); const result = try sink.applyCommands(&commands, &spawned); try expectMixedBatchResult(&world, existing, spawned[0], result); } world.deinit(); try std.testing.expectEqual(failing.allocated_bytes, failing.freed_bytes); if (completed_sweep) break; } try std.testing.expect(saw_operation_failure); try std.testing.expect(completed_sweep);}fn expectMixedBatchResult( world: *const WorldStore, existing: EntityId, spawned: EntityId, result: ApplyResult,) !void { try std.testing.expectEqual(@as(usize, 3), result.command_count); try std.testing.expectEqual(@as(usize, 1), result.spawned_count); try std.testing.expectEqual(@as(usize, 1), result.event_count); try std.testing.expectEqual(@as(usize, 1), result.visible_event_count); try std.testing.expectEqual(@as(u32, 2), world.aliveCount()); try std.testing.expectEqual(@as(f32, 9), world.transformDesc(existing).?.pos_x); try std.testing.expect(world.isAlive(spawned)); try std.testing.expectEqual(@as(f32, 3), world.transformDesc(spawned).?.pos_z); try std.testing.expectEqual(@as(u64, 42), world.visibleEvent(0).?.schema_id);}Audit
| Definitions | 26 |
|---|---|
| Public names | 26 |
| Members | 37 |
| Version | 26.7.0 |
| Revision | daab053ee433 |