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 }