lib/windowing/src/gamepad.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const sys = @import("sys");
  3 
  4 const rescan_poll_interval = 120;
  5 const gamecontroller = sys.apple.gamecontroller;
  6 
  7 const SourceKind = enum {
  8     evdev,
  9     gamecontroller,
 10     unsupported,
 11 };
 12 
 13 const source_kind: SourceKind = if (gamecontroller.supported)
 14     .gamecontroller
 15 else if (sys.evdev.supported)
 16     .evdev
 17 else
 18     .unsupported;
 19 
 20 pub const Limits = struct {
 21     retained_gamepad_count: usize = 4,
 22     retained_name_byte_count_per_gamepad: usize = 64,
 23     read_event_count: usize = 32,
 24     scanned_source_count: u32 = 32,
 25 };
 26 
 27 pub const CapacityError = error{
 28     GamepadStorageEmpty,
 29     NameStorageEmpty,
 30     NameStorageTooLarge,
 31     EventReadStorageEmpty,
 32     SourceScanEmpty,
 33     CapacityOverflow,
 34 };
 35 
 36 pub const Capacity = struct {
 37     retained_gamepad_count: usize,
 38     retained_name_byte_count_per_gamepad: usize,
 39     read_event_count: usize,
 40     scanned_source_count: u32,
 41     slot_bytes: usize,
 42     name_bytes: usize,
 43     read_event_bytes: usize,
 44     retained_storage_bytes: usize,
 45 
 46     pub fn derive(limits: Limits) CapacityError!Capacity {
 47         if (limits.retained_gamepad_count == 0) return error.GamepadStorageEmpty;
 48         if (limits.retained_name_byte_count_per_gamepad == 0) return error.NameStorageEmpty;
 49         if (source_kind == .evdev and
 50             limits.retained_name_byte_count_per_gamepad > sys.evdev.max_query_bytes)
 51         {
 52             return error.NameStorageTooLarge;
 53         }
 54         if (source_kind == .evdev and limits.read_event_count == 0) {
 55             return error.EventReadStorageEmpty;
 56         }
 57         if (limits.scanned_source_count == 0) return error.SourceScanEmpty;
 58         const slot_bytes = std.math.mul(
 59             usize,
 60             limits.retained_gamepad_count,
 61             @sizeOf(?Slot),
 62         ) catch return error.CapacityOverflow;
 63         const name_bytes = std.math.mul(
 64             usize,
 65             limits.retained_gamepad_count,
 66             limits.retained_name_byte_count_per_gamepad,
 67         ) catch return error.CapacityOverflow;
 68         const read_event_count = if (source_kind == .evdev) limits.read_event_count else 0;
 69         const read_event_bytes = if (source_kind == .evdev)
 70             std.math.mul(
 71                 usize,
 72                 read_event_count,
 73                 @sizeOf(sys.evdev.InputEvent),
 74             ) catch return error.CapacityOverflow
 75         else
 76             0;
 77         const retained_gamepad_bytes = std.math.add(
 78             usize,
 79             slot_bytes,
 80             name_bytes,
 81         ) catch return error.CapacityOverflow;
 82         const retained_storage_bytes = std.math.add(
 83             usize,
 84             retained_gamepad_bytes,
 85             read_event_bytes,
 86         ) catch return error.CapacityOverflow;
 87         return .{
 88             .retained_gamepad_count = limits.retained_gamepad_count,
 89             .retained_name_byte_count_per_gamepad = limits.retained_name_byte_count_per_gamepad,
 90             .read_event_count = read_event_count,
 91             .scanned_source_count = limits.scanned_source_count,
 92             .slot_bytes = slot_bytes,
 93             .name_bytes = name_bytes,
 94             .read_event_bytes = read_event_bytes,
 95             .retained_storage_bytes = retained_storage_bytes,
 96         };
 97     }
 98 };
 99 
100 pub const Status = struct {
101     rejected_discovery_count: u64,
102 };
103 
104 pub const Button = enum(u4) {
105     south = 0,
106     east = 1,
107     west = 2,
108     north = 3,
109     left_bumper = 4,
110     right_bumper = 5,
111     select = 6,
112     start = 7,
113     guide = 8,
114     left_thumb = 9,
115     right_thumb = 10,
116     dpad_up = 11,
117     dpad_down = 12,
118     dpad_left = 13,
119     dpad_right = 14,
120 };
121 
122 pub const Axis = enum(u3) {
123     left_x = 0,
124     left_y = 1,
125     right_x = 2,
126     right_y = 3,
127     left_trigger = 4,
128     right_trigger = 5,
129 };
130 
131 pub const button_count = @typeInfo(Button).@"enum".field_names.len;
132 pub const axis_count = @typeInfo(Axis).@"enum".field_names.len;
133 
134 pub const Gamepad = struct {
135     connected: bool = false,
136     name_bytes: []const u8 = &.{},
137     buttons: [button_count]bool = @splat(false),
138     pressed: [button_count]bool = @splat(false),
139     released: [button_count]bool = @splat(false),
140     axes: [axis_count]f32 = @splat(0),
141 
142     pub fn name(self: *const Gamepad) []const u8 {
143         return self.name_bytes;
144     }
145 
146     pub fn isDown(self: *const Gamepad, control: Button) bool {
147         return self.buttons[@backingInt(control)];
148     }
149 
150     pub fn isPressed(self: *const Gamepad, control: Button) bool {
151         return self.pressed[@backingInt(control)];
152     }
153 
154     pub fn isReleased(self: *const Gamepad, control: Button) bool {
155         return self.released[@backingInt(control)];
156     }
157 
158     pub fn axisValue(self: *const Gamepad, control: Axis) f32 {
159         return self.axes[@backingInt(control)];
160     }
161 
162     fn beginPoll(self: *Gamepad) void {
163         @memset(&self.pressed, false);
164         @memset(&self.released, false);
165     }
166 
167     fn setButton(self: *Gamepad, control: Button, down: bool) void {
168         const index = @backingInt(control);
169         if (down and !self.buttons[index]) self.pressed[index] = true;
170         if (!down and self.buttons[index]) self.released[index] = true;
171         self.buttons[index] = down;
172     }
173 
174     fn setAxis(self: *Gamepad, control: Axis, value: f32) void {
175         self.axes[@backingInt(control)] = value;
176     }
177 };
178 
179 const EvdevSlot = struct {
180     device: sys.evdev.Device,
181     event_index: u32 = 0,
182     pad: Gamepad = .{},
183     axis_ranges: [axis_count]sys.evdev.AbsInfo = @splat(.{
184         .value = 0,
185         .minimum = 0,
186         .maximum = 0,
187         .fuzz = 0,
188         .flat = 0,
189         .resolution = 0,
190     }),
191 };
192 
193 const GameControllerSlot = struct {
194     controller: gamecontroller.Controller,
195     seen: bool = false,
196     pad: Gamepad = .{},
197 };
198 
199 const UnsupportedSlot = struct {
200     pad: Gamepad = .{},
201 };
202 
203 const Slot = switch (source_kind) {
204     .evdev => EvdevSlot,
205     .gamecontroller => GameControllerSlot,
206     .unsupported => UnsupportedSlot,
207 };
208 
209 const GameControllerRuntime = if (gamecontroller.supported)
210     ?gamecontroller.Runtime
211 else
212     void;
213 
214 pub const State = struct {
215     allocator: std.mem.Allocator,
216     slots: []?Slot,
217     names: []u8,
218     read_events: []sys.evdev.InputEvent,
219     retained_name_byte_count_per_gamepad: usize,
220     scanned_source_count: u32,
221     gamecontroller_runtime: GameControllerRuntime,
222     polls_until_rescan: u32 = 0,
223     rejected_discovery_count: u64 = 0,
224     disconnected: Gamepad = .{},
225 
226     pub fn init(allocator: std.mem.Allocator, capacity: Capacity) std.mem.Allocator.Error!State {
227         std.debug.assert(capacity.retained_gamepad_count > 0);
228         std.debug.assert(capacity.retained_name_byte_count_per_gamepad > 0);
229         if (comptime source_kind == .evdev) {
230             std.debug.assert(
231                 capacity.retained_name_byte_count_per_gamepad <= sys.evdev.max_query_bytes,
232             );
233             std.debug.assert(capacity.read_event_count > 0);
234         } else {
235             std.debug.assert(capacity.read_event_count == 0);
236             std.debug.assert(capacity.read_event_bytes == 0);
237         }
238         std.debug.assert(capacity.scanned_source_count > 0);
239         std.debug.assert(capacity.slot_bytes % @sizeOf(?Slot) == 0);
240         std.debug.assert(
241             capacity.slot_bytes / @sizeOf(?Slot) == capacity.retained_gamepad_count,
242         );
243         std.debug.assert(
244             capacity.name_bytes % capacity.retained_name_byte_count_per_gamepad == 0,
245         );
246         std.debug.assert(
247             capacity.name_bytes / capacity.retained_name_byte_count_per_gamepad ==
248                 capacity.retained_gamepad_count,
249         );
250         if (comptime source_kind == .evdev) {
251             std.debug.assert(capacity.read_event_bytes % @sizeOf(sys.evdev.InputEvent) == 0);
252             std.debug.assert(
253                 capacity.read_event_bytes / @sizeOf(sys.evdev.InputEvent) ==
254                     capacity.read_event_count,
255             );
256         }
257         const slots = try allocator.alloc(?Slot, capacity.retained_gamepad_count);
258         errdefer allocator.free(slots);
259         @memset(slots, null);
260         const names = try allocator.alloc(u8, capacity.name_bytes);
261         errdefer allocator.free(names);
262         const read_events: []sys.evdev.InputEvent = if (comptime source_kind == .evdev)
263             try allocator.alloc(sys.evdev.InputEvent, capacity.read_event_count)
264         else
265             @constCast((&[_]sys.evdev.InputEvent{})[0..]);
266         return .{
267             .allocator = allocator,
268             .slots = slots,
269             .names = names,
270             .read_events = read_events,
271             .retained_name_byte_count_per_gamepad = capacity.retained_name_byte_count_per_gamepad,
272             .scanned_source_count = capacity.scanned_source_count,
273             .gamecontroller_runtime = if (comptime gamecontroller.supported)
274                 gamecontroller.Runtime.init()
275             else {},
276         };
277     }
278 
279     pub fn deinit(self: *State) void {
280         std.debug.assert(self.slots.len > 0);
281         std.debug.assert(
282             self.names.len == self.slots.len * self.retained_name_byte_count_per_gamepad,
283         );
284         if (comptime source_kind == .evdev) {
285             std.debug.assert(self.read_events.len > 0);
286         } else {
287             std.debug.assert(self.read_events.len == 0);
288         }
289         for (self.slots) |*slot| releaseSlot(slot);
290         if (comptime source_kind == .evdev) self.allocator.free(self.read_events);
291         self.allocator.free(self.names);
292         self.allocator.free(self.slots);
293         self.* = undefined;
294     }
295 
296     pub fn poll(self: *State) void {
297         switch (comptime source_kind) {
298             .evdev => self.pollEvdev(),
299             .gamecontroller => self.pollGameControllers(),
300             .unsupported => {},
301         }
302     }
303 
304     fn pollEvdev(self: *State) void {
305         if (self.polls_until_rescan == 0) {
306             self.rescan();
307             self.polls_until_rescan = rescan_poll_interval;
308         }
309         self.polls_until_rescan -= 1;
310 
311         for (self.slots) |*slot_ref| {
312             const slot = &(slot_ref.* orelse continue);
313             slot.pad.beginPoll();
314             while (true) {
315                 const count = slot.device.readEvents(self.read_events) catch {
316                     slot.device.close();
317                     slot_ref.* = null;
318                     break;
319                 };
320                 if (count == 0) break;
321                 for (self.read_events[0..count]) |event| {
322                     applyEvent(slot, event);
323                 }
324             }
325         }
326     }
327 
328     fn pollGameControllers(self: *State) void {
329         const runtime = if (self.gamecontroller_runtime) |*value| value else return;
330         beginGameControllerPoll(self);
331         var controllers = runtime.controllers();
332         defer controllers.deinit();
333 
334         const scan_count = @min(
335             controllers.count,
336             @as(usize, self.scanned_source_count),
337         );
338         std.debug.assert(scan_count <= @as(usize, self.scanned_source_count));
339         var index: usize = 0;
340         while (index < scan_count) : (index += 1) {
341             updateTrackedGameController(self, runtime, controllers.at(index));
342         }
343         finishGameControllerPoll(self);
344         index = 0;
345         while (index < scan_count) : (index += 1) {
346             adoptGameController(self, runtime, controllers.at(index));
347         }
348         if (controllers.count > scan_count) {
349             self.rejectDiscoveries(controllers.count - scan_count);
350         }
351     }
352 
353     pub fn rescan(self: *State) void {
354         if (comptime !sys.evdev.supported) return;
355         var index: u32 = 0;
356         while (index < self.scanned_source_count) : (index += 1) {
357             var path_buffer: [32]u8 = undefined;
358             const path = sys.evdev.eventPath(&path_buffer, index) catch continue;
359             if (self.tracksEventIndex(index)) continue;
360             const device = sys.evdev.Device.open(path) catch continue;
361             if (!adoptEvdev(self, device, index)) device.close();
362         }
363     }
364 
365     fn tracksEventIndex(self: *const State, index: u32) bool {
366         for (self.slots) |slot| {
367             if (slot) |held| {
368                 if (held.event_index == index) return true;
369             }
370         }
371         return false;
372     }
373 
374     pub fn connectedCount(self: *const State) usize {
375         var count: usize = 0;
376         for (self.slots) |slot| {
377             if (slot != null) count += 1;
378         }
379         return count;
380     }
381 
382     pub fn gamepad(self: *const State, index: usize) *const Gamepad {
383         if (index >= self.slots.len) return &self.disconnected;
384         if (self.slots[index]) |*slot| return &slot.pad;
385         return &self.disconnected;
386     }
387 
388     pub fn status(self: *const State) Status {
389         return .{ .rejected_discovery_count = self.rejected_discovery_count };
390     }
391 
392     fn availableSlotIndex(self: *State) ?usize {
393         for (self.slots, 0..) |slot, index| {
394             if (slot == null) return index;
395         }
396         self.rejected_discovery_count +|= 1;
397         return null;
398     }
399 
400     fn rejectDiscoveries(self: *State, count: usize) void {
401         const increment = std.math.cast(u64, count) orelse std.math.maxInt(u64);
402         self.rejected_discovery_count +|= increment;
403     }
404 
405     fn nameStorage(self: *State, index: usize) []u8 {
406         std.debug.assert(index < self.slots.len);
407         const start = index * self.retained_name_byte_count_per_gamepad;
408         const end = start + self.retained_name_byte_count_per_gamepad;
409         std.debug.assert(end <= self.names.len);
410         return self.names[start..end];
411     }
412 };
413 
414 fn releaseSlot(slot: *?Slot) void {
415     if (slot.*) |held| switch (comptime source_kind) {
416         .evdev => held.device.close(),
417         .gamecontroller => held.controller.release(),
418         .unsupported => {},
419     };
420     slot.* = null;
421 }
422 
423 fn adoptEvdev(state: *State, device: sys.evdev.Device, index: u32) bool {
424     var key_bits: sys.evdev.KeyBits = undefined;
425     device.keyBits(&key_bits) catch return false;
426     if (!sys.evdev.isGamepad(&key_bits)) return false;
427 
428     const slot_index = state.availableSlotIndex() orelse return false;
429     var slot = EvdevSlot{ .device = device, .event_index = index };
430     slot.pad.connected = true;
431     const name_storage = state.nameStorage(slot_index);
432     if (device.name(name_storage)) |device_name| {
433         slot.pad.name_bytes = device_name;
434     } else |_| {}
435     for (axis_codes, 0..) |code, axis_index| {
436         if (device.absInfo(code)) |info| {
437             slot.axis_ranges[axis_index] = info;
438         } else |_| {}
439     }
440     state.slots[slot_index] = slot;
441     return true;
442 }
443 
444 fn beginGameControllerPoll(state: *State) void {
445     for (state.slots) |*slot_ref| {
446         const slot = &(slot_ref.* orelse continue);
447         slot.seen = false;
448         slot.pad.beginPoll();
449     }
450 }
451 
452 fn updateTrackedGameController(
453     state: *State,
454     runtime: *const gamecontroller.Runtime,
455     controller: gamecontroller.Controller,
456 ) void {
457     const profile = runtime.extendedGamepad(controller) orelse return;
458     for (state.slots) |*slot_ref| {
459         const slot = &(slot_ref.* orelse continue);
460         if (!slot.controller.eql(controller)) continue;
461         slot.seen = true;
462         applyGameControllerProfile(runtime, &slot.pad, profile);
463         return;
464     }
465 }
466 
467 fn adoptGameController(
468     state: *State,
469     runtime: *const gamecontroller.Runtime,
470     controller: gamecontroller.Controller,
471 ) void {
472     const profile = runtime.extendedGamepad(controller) orelse return;
473     for (state.slots) |slot| {
474         const held = slot orelse continue;
475         if (held.controller.eql(controller)) return;
476     }
477 
478     const slot_index = state.availableSlotIndex() orelse return;
479     var slot = GameControllerSlot{
480         .controller = controller.retain(),
481         .seen = true,
482     };
483     slot.pad.connected = true;
484     slot.pad.name_bytes = copyGameControllerName(state, slot_index, runtime, controller);
485     applyGameControllerProfile(runtime, &slot.pad, profile);
486     state.slots[slot_index] = slot;
487 }
488 
489 fn finishGameControllerPoll(state: *State) void {
490     for (state.slots) |*slot_ref| {
491         const slot = &(slot_ref.* orelse continue);
492         if (!slot.seen) releaseSlot(slot_ref);
493     }
494 }
495 
496 fn copyGameControllerName(
497     state: *State,
498     slot_index: usize,
499     runtime: *const gamecontroller.Runtime,
500     controller: gamecontroller.Controller,
501 ) []const u8 {
502     const fallback = "Game Controller";
503     const source = if (runtime.controllerName(controller)) |name|
504         std.mem.span(name)
505     else
506         fallback;
507     const storage = state.nameStorage(slot_index);
508     const byte_count = @min(source.len, storage.len);
509     @memcpy(storage[0..byte_count], source[0..byte_count]);
510     return storage[0..byte_count];
511 }
512 
513 const gamecontroller_button_map = [_]struct {
514     source: gamecontroller.Button,
515     target: Button,
516 }{
517     .{ .source = .a, .target = .south },
518     .{ .source = .b, .target = .east },
519     .{ .source = .x, .target = .west },
520     .{ .source = .y, .target = .north },
521     .{ .source = .left_shoulder, .target = .left_bumper },
522     .{ .source = .right_shoulder, .target = .right_bumper },
523     .{ .source = .options, .target = .select },
524     .{ .source = .menu, .target = .start },
525     .{ .source = .home, .target = .guide },
526     .{ .source = .left_thumbstick, .target = .left_thumb },
527     .{ .source = .right_thumbstick, .target = .right_thumb },
528     .{ .source = .dpad_up, .target = .dpad_up },
529     .{ .source = .dpad_down, .target = .dpad_down },
530     .{ .source = .dpad_left, .target = .dpad_left },
531     .{ .source = .dpad_right, .target = .dpad_right },
532 };
533 
534 const gamecontroller_axis_map = [_]struct {
535     source: gamecontroller.Axis,
536     target: Axis,
537     scale: f32,
538 }{
539     .{ .source = .left_thumbstick_x, .target = .left_x, .scale = 1 },
540     .{ .source = .left_thumbstick_y, .target = .left_y, .scale = -1 },
541     .{ .source = .right_thumbstick_x, .target = .right_x, .scale = 1 },
542     .{ .source = .right_thumbstick_y, .target = .right_y, .scale = -1 },
543     .{ .source = .left_trigger, .target = .left_trigger, .scale = 1 },
544     .{ .source = .right_trigger, .target = .right_trigger, .scale = 1 },
545 };
546 
547 fn applyGameControllerProfile(
548     runtime: *const gamecontroller.Runtime,
549     pad: *Gamepad,
550     profile: gamecontroller.Profile,
551 ) void {
552     for (gamecontroller_button_map) |mapping| {
553         pad.setButton(mapping.target, runtime.buttonPressed(profile, mapping.source));
554     }
555     for (gamecontroller_axis_map) |mapping| {
556         pad.setAxis(
557             mapping.target,
558             runtime.axisValue(profile, mapping.source) * mapping.scale,
559         );
560     }
561 }
562 
563 const axis_codes = [axis_count]u16{
564     sys.evdev.axis.x,
565     sys.evdev.axis.y,
566     sys.evdev.axis.rx,
567     sys.evdev.axis.ry,
568     sys.evdev.axis.z,
569     sys.evdev.axis.rz,
570 };
571 
572 fn applyEvent(slot: *EvdevSlot, event: sys.evdev.InputEvent) void {
573     switch (event.kind) {
574         sys.evdev.event_kind.key => {
575             const control = buttonFromCode(event.code) orelse return;
576             slot.pad.setButton(control, event.value != 0);
577         },
578         sys.evdev.event_kind.abs => applyAbs(slot, event.code, event.value),
579         else => {},
580     }
581 }
582 
583 fn applyAbs(slot: *EvdevSlot, code: u16, value: i32) void {
584     switch (code) {
585         sys.evdev.axis.hat0x => {
586             slot.pad.setButton(.dpad_left, value < 0);
587             slot.pad.setButton(.dpad_right, value > 0);
588         },
589         sys.evdev.axis.hat0y => {
590             slot.pad.setButton(.dpad_up, value < 0);
591             slot.pad.setButton(.dpad_down, value > 0);
592         },
593         else => {
594             const control = axisFromCode(code) orelse return;
595             const info = slot.axis_ranges[@backingInt(control)];
596             const normalized = switch (control) {
597                 .left_trigger, .right_trigger => sys.evdev.normalizeTrigger(info, value),
598                 else => sys.evdev.normalizeAxis(info, value),
599             };
600             slot.pad.setAxis(control, normalized);
601         },
602     }
603 }
604 
605 fn buttonFromCode(code: u16) ?Button {
606     return switch (code) {
607         sys.evdev.button.south => .south,
608         sys.evdev.button.east => .east,
609         sys.evdev.button.west => .west,
610         sys.evdev.button.north => .north,
611         sys.evdev.button.left_bumper => .left_bumper,
612         sys.evdev.button.right_bumper => .right_bumper,
613         sys.evdev.button.select => .select,
614         sys.evdev.button.start => .start,
615         sys.evdev.button.mode => .guide,
616         sys.evdev.button.left_thumb => .left_thumb,
617         sys.evdev.button.right_thumb => .right_thumb,
618         sys.evdev.button.dpad_up => .dpad_up,
619         sys.evdev.button.dpad_down => .dpad_down,
620         sys.evdev.button.dpad_left => .dpad_left,
621         sys.evdev.button.dpad_right => .dpad_right,
622         else => null,
623     };
624 }
625 
626 fn axisFromCode(code: u16) ?Axis {
627     return switch (code) {
628         sys.evdev.axis.x => .left_x,
629         sys.evdev.axis.y => .left_y,
630         sys.evdev.axis.rx => .right_x,
631         sys.evdev.axis.ry => .right_y,
632         sys.evdev.axis.z => .left_trigger,
633         sys.evdev.axis.rz => .right_trigger,
634         else => null,
635     };
636 }
637 
638 fn initTestState(limits: Limits) !State {
639     return State.init(std.testing.allocator, try Capacity.derive(limits));
640 }
641 
642 test "gamepad capacity derives exact storage and rejects invalid limits" {
643     const capacity = try Capacity.derive(.{});
644     const expected_read_event_count: usize = if (source_kind == .evdev) 32 else 0;
645     const expected_read_event_bytes = expected_read_event_count *
646         @sizeOf(sys.evdev.InputEvent);
647     try std.testing.expectEqual(@as(usize, 4), capacity.retained_gamepad_count);
648     try std.testing.expectEqual(@as(usize, 64), capacity.retained_name_byte_count_per_gamepad);
649     try std.testing.expectEqual(expected_read_event_count, capacity.read_event_count);
650     try std.testing.expectEqual(@as(u32, 32), capacity.scanned_source_count);
651     try std.testing.expectEqual(4 * @sizeOf(?Slot), capacity.slot_bytes);
652     try std.testing.expectEqual(@as(usize, 4 * 64), capacity.name_bytes);
653     try std.testing.expectEqual(expected_read_event_bytes, capacity.read_event_bytes);
654     try std.testing.expectEqual(
655         capacity.slot_bytes + capacity.name_bytes + capacity.read_event_bytes,
656         capacity.retained_storage_bytes,
657     );
658     try std.testing.expectError(error.GamepadStorageEmpty, Capacity.derive(.{
659         .retained_gamepad_count = 0,
660     }));
661     try std.testing.expectError(error.NameStorageEmpty, Capacity.derive(.{
662         .retained_name_byte_count_per_gamepad = 0,
663     }));
664     if (comptime source_kind == .evdev) {
665         try std.testing.expectError(error.NameStorageTooLarge, Capacity.derive(.{
666             .retained_name_byte_count_per_gamepad = sys.evdev.max_query_bytes + 1,
667         }));
668         try std.testing.expectError(error.EventReadStorageEmpty, Capacity.derive(.{
669             .read_event_count = 0,
670         }));
671     }
672     try std.testing.expectError(error.SourceScanEmpty, Capacity.derive(.{
673         .scanned_source_count = 0,
674     }));
675     try std.testing.expectError(error.CapacityOverflow, Capacity.derive(.{
676         .retained_gamepad_count = std.math.maxInt(usize) / @sizeOf(?Slot) + 1,
677     }));
678     try std.testing.expectError(error.CapacityOverflow, Capacity.derive(.{
679         .retained_gamepad_count = std.math.maxInt(usize) / sys.evdev.max_query_bytes + 1,
680         .retained_name_byte_count_per_gamepad = sys.evdev.max_query_bytes,
681     }));
682     if (comptime source_kind == .evdev) {
683         try std.testing.expectError(error.CapacityOverflow, Capacity.derive(.{
684             .read_event_count = std.math.maxInt(usize) / @sizeOf(sys.evdev.InputEvent) + 1,
685         }));
686     }
687     try std.testing.expectError(error.CapacityOverflow, Capacity.derive(.{
688         .retained_gamepad_count = std.math.maxInt(usize) / sys.evdev.max_query_bytes,
689         .retained_name_byte_count_per_gamepad = sys.evdev.max_query_bytes,
690     }));
691 }
692 
693 test "gamepad storage retains nondefault limits and rejects max plus one" {
694     const capacity = try Capacity.derive(.{
695         .retained_gamepad_count = 2,
696         .retained_name_byte_count_per_gamepad = 5,
697         .read_event_count = 3,
698         .scanned_source_count = 7,
699     });
700     const allocation_count: usize = if (source_kind == .evdev) 3 else 2;
701     var failing = std.testing.FailingAllocator.init(
702         std.testing.allocator,
703         .{ .fail_index = allocation_count },
704     );
705     var state = try State.init(failing.allocator(), capacity);
706     defer state.deinit();
707     try std.testing.expectEqual(allocation_count, failing.allocations);
708     try std.testing.expectEqual(@as(usize, 2), state.slots.len);
709     try std.testing.expectEqual(@as(usize, 10), state.names.len);
710     const expected_read_event_count: usize = if (source_kind == .evdev) 3 else 0;
711     try std.testing.expectEqual(expected_read_event_count, state.read_events.len);
712     try std.testing.expectEqual(@as(u32, 7), state.scanned_source_count);
713     const read_events_pointer = state.read_events.ptr;
714     state.polls_until_rescan = 1;
715     state.poll();
716     try std.testing.expectEqual(read_events_pointer, state.read_events.ptr);
717     try std.testing.expectEqual(allocation_count, failing.allocations);
718 
719     const first_name = state.nameStorage(0);
720     const second_name = state.nameStorage(1);
721     try std.testing.expectEqual(@as(usize, 5), first_name.len);
722     try std.testing.expectEqual(@as(usize, 5), second_name.len);
723     try std.testing.expectEqual(@intFromPtr(first_name.ptr) + 5, @intFromPtr(second_name.ptr));
724     @memcpy(first_name[0..3], "one");
725     @memcpy(second_name[0..3], "two");
726 
727     if (comptime source_kind != .evdev) return;
728 
729     const first_index = state.availableSlotIndex().?;
730     state.slots[first_index] = EvdevSlot{
731         .device = .{ .fd = -1 },
732         .event_index = 3,
733         .pad = .{ .connected = true, .name_bytes = first_name[0..3] },
734     };
735     const second_index = state.availableSlotIndex().?;
736     state.slots[second_index] = EvdevSlot{
737         .device = .{ .fd = -1 },
738         .event_index = 5,
739         .pad = .{ .connected = true, .name_bytes = second_name[0..3] },
740     };
741     try std.testing.expectEqual(@as(usize, 2), state.connectedCount());
742     try std.testing.expectEqualStrings("one", state.gamepad(0).name());
743     try std.testing.expectEqualStrings("two", state.gamepad(1).name());
744     try std.testing.expect(state.availableSlotIndex() == null);
745     try std.testing.expectEqual(Status{ .rejected_discovery_count = 1 }, state.status());
746     state.rejected_discovery_count = std.math.maxInt(u64);
747     try std.testing.expect(state.availableSlotIndex() == null);
748     try std.testing.expectEqual(
749         Status{ .rejected_discovery_count = std.math.maxInt(u64) },
750         state.status(),
751     );
752     try std.testing.expectEqual(@as(u32, 3), state.slots[0].?.event_index);
753     try std.testing.expectEqual(@as(u32, 5), state.slots[1].?.event_index);
754     state.slots[0] = null;
755     try std.testing.expectEqual(@as(usize, 0), state.availableSlotIndex().?);
756     try std.testing.expectEqual(
757         Status{ .rejected_discovery_count = std.math.maxInt(u64) },
758         state.status(),
759     );
760     try std.testing.expectEqual(allocation_count, failing.allocations);
761     @memset(state.slots, null);
762 }
763 
764 test "gamepad storage initialization cleans up every allocation failure" {
765     const capacity = try Capacity.derive(.{
766         .retained_gamepad_count = 2,
767         .retained_name_byte_count_per_gamepad = 5,
768         .read_event_count = 3,
769     });
770     const allocation_count: usize = if (source_kind == .evdev) 3 else 2;
771     var fail_index: usize = 0;
772     while (fail_index < allocation_count) : (fail_index += 1) {
773         var failing = std.testing.FailingAllocator.init(
774             std.testing.allocator,
775             .{ .fail_index = fail_index },
776         );
777         try std.testing.expectError(error.OutOfMemory, State.init(failing.allocator(), capacity));
778     }
779 
780     var state = try State.init(std.testing.allocator, capacity);
781     state.deinit();
782 }
783 
784 test "buttons track press and release edges" {
785     var pad = Gamepad{ .connected = true };
786     pad.beginPoll();
787     pad.setButton(.south, true);
788     try std.testing.expect(pad.isDown(.south));
789     try std.testing.expect(pad.isPressed(.south));
790     try std.testing.expect(!pad.isReleased(.south));
791 
792     pad.beginPoll();
793     pad.setButton(.south, true);
794     try std.testing.expect(!pad.isPressed(.south));
795 
796     pad.beginPoll();
797     pad.setButton(.south, false);
798     try std.testing.expect(!pad.isDown(.south));
799     try std.testing.expect(pad.isReleased(.south));
800 }
801 
802 test "hat events become dpad buttons and axes normalize" {
803     if (comptime source_kind != .evdev) return error.SkipZigTest;
804 
805     var slot = EvdevSlot{ .device = .{ .fd = -1 } };
806     slot.axis_ranges[@backingInt(Axis.left_x)] = .{
807         .value = 0,
808         .minimum = -32768,
809         .maximum = 32767,
810         .fuzz = 0,
811         .flat = 0,
812         .resolution = 0,
813     };
814 
815     applyAbs(&slot, sys.evdev.axis.hat0x, -1);
816     try std.testing.expect(slot.pad.isDown(.dpad_left));
817     applyAbs(&slot, sys.evdev.axis.hat0x, 0);
818     try std.testing.expect(!slot.pad.isDown(.dpad_left));
819 
820     applyAbs(&slot, sys.evdev.axis.x, 32767);
821     try std.testing.expectApproxEqAbs(@as(f32, 1.0), slot.pad.axisValue(.left_x), 0.001);
822 
823     applyEvent(&slot, .{
824         .seconds = 0,
825         .microseconds = 0,
826         .kind = sys.evdev.event_kind.key,
827         .code = sys.evdev.button.start,
828         .value = 1,
829     });
830     try std.testing.expect(slot.pad.isDown(.start));
831 }
832 
833 test "GameController snapshot maps standard buttons and axes" {
834     if (comptime source_kind != .gamecontroller) return error.SkipZigTest;
835 
836     var state = try initTestState(.{});
837     defer state.deinit();
838     const runtime = gamecontroller.Runtime.init().?;
839     const controller = runtime.createRetainedSnapshot().?;
840     defer controller.release();
841     const profile = runtime.extendedGamepad(controller).?;
842     const button_cases = [_]struct {
843         source: gamecontroller.Button,
844         target: Button,
845     }{
846         .{ .source = .a, .target = .south },
847         .{ .source = .b, .target = .east },
848         .{ .source = .x, .target = .west },
849         .{ .source = .y, .target = .north },
850         .{ .source = .left_shoulder, .target = .left_bumper },
851         .{ .source = .right_shoulder, .target = .right_bumper },
852         .{ .source = .menu, .target = .start },
853         .{ .source = .dpad_up, .target = .dpad_up },
854     };
855     for (button_cases) |case| {
856         if (case.source == .dpad_up) continue;
857         try std.testing.expect(runtime.setButtonValue(profile, case.source, 1));
858     }
859     try std.testing.expect(runtime.setDirectionPadValue(profile, 0, 1));
860     try std.testing.expect(runtime.setAxisValue(profile, .left_thumbstick_x, 0.25));
861     try std.testing.expect(runtime.setAxisValue(profile, .left_thumbstick_y, 0.5));
862     try std.testing.expect(runtime.setAxisValue(profile, .right_thumbstick_y, -0.75));
863     try std.testing.expect(runtime.setAxisValue(profile, .right_trigger, 0.6));
864 
865     beginGameControllerPoll(&state);
866     finishGameControllerPoll(&state);
867     adoptGameController(&state, &runtime, controller);
868     const pad = state.gamepad(0);
869     for (button_cases) |case| {
870         try std.testing.expect(pad.isDown(case.target));
871         try std.testing.expect(pad.isPressed(case.target));
872     }
873     try std.testing.expect(pad.name().len > 0);
874     try std.testing.expectApproxEqAbs(@as(f32, 0.25), pad.axisValue(.left_x), 0.001);
875     try std.testing.expectApproxEqAbs(@as(f32, -0.5), pad.axisValue(.left_y), 0.001);
876     try std.testing.expectApproxEqAbs(@as(f32, 0.75), pad.axisValue(.right_y), 0.001);
877     try std.testing.expectApproxEqAbs(@as(f32, 0.6), pad.axisValue(.right_trigger), 0.001);
878 }
879 
880 test "GameController reconciliation preserves edges and replaces disconnected slots" {
881     if (comptime source_kind != .gamecontroller) return error.SkipZigTest;
882 
883     var state = try initTestState(.{ .retained_gamepad_count = 1 });
884     defer state.deinit();
885     const runtime = gamecontroller.Runtime.init().?;
886     const first = runtime.createRetainedSnapshot().?;
887     defer first.release();
888     const second = runtime.createRetainedSnapshot().?;
889     defer second.release();
890     const first_profile = runtime.extendedGamepad(first).?;
891     try std.testing.expect(runtime.setButtonValue(first_profile, .a, 1));
892 
893     beginGameControllerPoll(&state);
894     updateTrackedGameController(&state, &runtime, first);
895     updateTrackedGameController(&state, &runtime, second);
896     finishGameControllerPoll(&state);
897     adoptGameController(&state, &runtime, first);
898     adoptGameController(&state, &runtime, second);
899     try std.testing.expectEqual(@as(usize, 1), state.connectedCount());
900     try std.testing.expect(state.gamepad(0).isPressed(.south));
901     try std.testing.expectEqual(Status{ .rejected_discovery_count = 1 }, state.status());
902 
903     beginGameControllerPoll(&state);
904     updateTrackedGameController(&state, &runtime, first);
905     finishGameControllerPoll(&state);
906     adoptGameController(&state, &runtime, first);
907     try std.testing.expect(!state.gamepad(0).isPressed(.south));
908     try std.testing.expect(runtime.setButtonValue(first_profile, .a, 0));
909     beginGameControllerPoll(&state);
910     updateTrackedGameController(&state, &runtime, first);
911     finishGameControllerPoll(&state);
912     try std.testing.expect(state.gamepad(0).isReleased(.south));
913 
914     beginGameControllerPoll(&state);
915     updateTrackedGameController(&state, &runtime, second);
916     finishGameControllerPoll(&state);
917     adoptGameController(&state, &runtime, second);
918     try std.testing.expectEqual(@as(usize, 1), state.connectedCount());
919     try std.testing.expectEqual(Status{ .rejected_discovery_count = 1 }, state.status());
920     beginGameControllerPoll(&state);
921     finishGameControllerPoll(&state);
922     try std.testing.expectEqual(@as(usize, 0), state.connectedCount());
923 }
924 
925 test "state hands out a disconnected pad for empty slots" {
926     var state = try initTestState(.{});
927     defer state.deinit();
928     try std.testing.expectEqual(@as(usize, 0), state.connectedCount());
929     try std.testing.expect(!state.gamepad(0).connected);
930     try std.testing.expect(!state.gamepad(99).connected);
931 }
932 
933 test "live GameController connection enters bounded state" {
934     if (comptime source_kind != .gamecontroller) return error.SkipZigTest;
935 
936     var state = try initTestState(.{});
937     defer state.deinit();
938     state.poll();
939     if (state.connectedCount() == 0) return error.SkipZigTest;
940 
941     const pad = state.gamepad(0);
942     try std.testing.expect(pad.connected);
943     try std.testing.expect(pad.name().len > 0);
944     try std.testing.expect(pad.axisValue(.left_x) >= -1);
945     try std.testing.expect(pad.axisValue(.left_x) <= 1);
946     try std.testing.expect(pad.axisValue(.left_y) >= -1);
947     try std.testing.expect(pad.axisValue(.left_y) <= 1);
948     try std.testing.expect(pad.axisValue(.left_trigger) >= 0);
949     try std.testing.expect(pad.axisValue(.left_trigger) <= 1);
950 }
951 
952 test "live gamepad reports buttons and axes" {
953     if (comptime !sys.evdev.supported) return error.SkipZigTest;
954 
955     var state = try initTestState(.{});
956     defer state.deinit();
957     state.rescan();
958     if (state.connectedCount() == 0) return error.SkipZigTest;
959 
960     var saw_south = false;
961     var saw_axis = false;
962     var waited_ms: u32 = 0;
963     while (waited_ms < 5000 and !(saw_south and saw_axis)) : (waited_ms += 20) {
964         state.poll();
965         const pad = state.gamepad(0);
966         if (pad.isDown(.south) or pad.isPressed(.south)) saw_south = true;
967         if (@abs(pad.axisValue(.left_x)) > 0.9) saw_axis = true;
968         sys.time.sleepMilliseconds(20);
969     }
970 
971     try std.testing.expect(state.gamepad(0).name().len > 0);
972     try std.testing.expect(saw_south);
973     try std.testing.expect(saw_axis);
974 }