2026-07-22 18:09:06 +02:00
|
|
|
//SPDX-License-Identifier: LGPL-3.0-only
|
|
|
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
2026-05-12 00:24:48 +02:00
|
|
|
|
|
|
|
|
module;
|
|
|
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
|
|
|
|
#include <libudev.h>
|
|
|
|
|
#include <libevdev/libevdev.h>
|
|
|
|
|
#include <linux/input.h>
|
|
|
|
|
#include <linux/input-event-codes.h>
|
|
|
|
|
#include <fcntl.h>
|
|
|
|
|
#include <unistd.h>
|
|
|
|
|
#include <poll.h>
|
|
|
|
|
#include <sys/ioctl.h>
|
|
|
|
|
#include <cstring>
|
|
|
|
|
#endif
|
|
|
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
feat: windows on their own clock, deferred deletion for every handle owner, staged re-uploads, and RTPass push data
Window
- updateInterval runs Update on the window's own clock while the
compositor sends no frame callbacks (hidden, minimized): the Wayland loop
is a poll() loop, and the Win32 loop sleeps until the next update. Both
read the interval each pass.
- SetPresentMode (FIFO, Mailbox, Immediate). A mode the surface lacks, or
whose swapchain has other than numFrames images (Mesa's Mailbox has four),
falls back to FIFO until the mode is set again.
- AddPollFd / RemovePollFd wake the Wayland loop on the app's descriptors.
- The swapchain has TRANSFER_DST where the surface allows it.
- CaptureNextFrame replaces SaveFrame: it records the copy in the frame,
before the present, where SaveFrame read an image already presented. A
frame unwound past before the write still frees the readback buffer.
- Inter-pass barriers on the swapchain image carry the access masks of the
stages the passes use.
Deferred deletion (issue #101)
- VulkanBuffer, Mesh, VulkanShader, ComputeShader and PipelineRTVulkan hand
their handles to the deletion queue from their destructors and moves, as
a frame in flight may still use them; a static owner goes after the
driver has torn down. VulkanShader and PipelineRTVulkan move and can't be
copied; re-Load and re-Init queue the old pipeline.
- The queue and its mutex are never destroyed, and the queue takes handles
from any thread; Device::frameCounter is atomic. Both the queue and the
staging rings count frames as one window renders them (documented).
- DrainDeletions also lets every buffer's staging ring start over: without
a Window nothing else reclaims either, so an app without one calls it
after waiting for its work.
Meshes and buffers
- A Mesh rebuild queues the old acceleration structure and builds into the
same storage after a barrier, so the BLAS address stays put when it fits.
The refit and rebuild barriers cover compute ray queries too.
- RTBuildOptions::noDuplicateAnyHit. Mesh::geometryFlags replaces opaque,
and the refit and rebuild fallbacks keep the flags.
- A re-upload into a buffer a frame in flight may read (UploadDeviceLocal
of the same size, UploadDeviceLocalRange) copies on the GPU, after those
reads, through a per-frame staging ring; on ReBAR it used to write the
buffer in place. Empty uploads record nothing.
- A compressed Mesh::Build that keeps its buffers decompresses into them
after the reads of frames in flight, and the hit shaders and ray queries
wait for the decompress as well as the BLAS build. Its vertex buffer
takes a ranged refit's copies.
- Mesh move-assigns (queueing the BLAS it replaces), so it can be replaced
in place or erased from a vector.
- VulkanBuffer::Create asks for a device address only for buffers with
SHADER_DEVICE_ADDRESS usage, as vkGetBufferDeviceAddress requires; the
others have address 0. UIRenderer::RegisterBuffer refuses those.
- Build inputs are synchronized as shader reads.
Other
- RTPass pushes the app's own push data and traces at an explicit size.
- DescriptorHeapVulkan hands freed slot ranges out again (they leaked), and
keeps its free lists sorted.
- ImageVulkan's mip chain clamps each level to 1 texel: a non-square
image's shorter side went to 0.
- VulkanShader throws on a missing file instead of failing in bad_alloc.
- CRAFTER_GRAPHICS_DEVICE picks the GPU by its number among the distinct
GPUs Vulkan lists; anything but such a number is an error.
- The debug messenger prints info messages only with
CRAFTER_GRAPHICS_VALIDATION set.
- Windows: the WinRT gamepad code moved to a C file (mingw-w64's WinRT
headers don't compile as C++), -lcombase dropped, a reference leak fixed.
- project.cpp honours --local for Crafter.Event, Math and Asset, as Asset
does for Math, and builds every test through one helper, projectArgs
included.
- Tests: DebugMessengerSeverity, DeletionQueueThreads, DeviceSelection,
HeapSlotReuse, MeshLifetime, MipChainNonSquare, PipelineLifetime,
RTPassPushData, SwapchainTransferDst, UploadInFlight, VulkanBufferAddress,
VulkanShaderLifetime, WindowUpdateClock; others extended.
Breaking: SaveFrame is CaptureNextFrame; Mesh::opaque is geometryFlags; a
buffer without SHADER_DEVICE_ADDRESS usage has no address; frameCounter is
std::atomic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:54 +02:00
|
|
|
#include "Crafter.Graphics-GamepadWin32.h"
|
2026-05-12 00:24:48 +02:00
|
|
|
#endif
|
|
|
|
|
module Crafter.Graphics;
|
|
|
|
|
import std;
|
|
|
|
|
|
|
|
|
|
using namespace Crafter;
|
|
|
|
|
|
|
|
|
|
// ────────────────────────────────────────────────────────────────────
|
|
|
|
|
// Linux backend (libudev + libevdev)
|
|
|
|
|
// ────────────────────────────────────────────────────────────────────
|
|
|
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
|
|
|
|
namespace {
|
|
|
|
|
// Per-device runtime state. The public `Gamepad::Device` carries the
|
|
|
|
|
// identity + polled state + events; this struct holds the libevdev /
|
|
|
|
|
// fd / mapping tables we need to drain events from.
|
|
|
|
|
struct LinuxBackend {
|
|
|
|
|
libevdev* evdev = nullptr;
|
|
|
|
|
int fd = -1;
|
|
|
|
|
// Per-axis calibration (libevdev's min/max so we can normalize).
|
|
|
|
|
// Indexed by Gamepad::Axis. -1 in `code` means "this axis isn't
|
|
|
|
|
// exposed by this device".
|
|
|
|
|
struct AxisInfo {
|
|
|
|
|
int code = -1; // kernel ABS_* code, or -1
|
|
|
|
|
int min = 0;
|
|
|
|
|
int max = 0;
|
|
|
|
|
float flat = 0.0f; // deadzone hint (raw units)
|
|
|
|
|
bool invertY = false; // negate output (stick Y by convention)
|
|
|
|
|
};
|
|
|
|
|
AxisInfo axisInfo[(std::size_t)Crafter::Gamepad::Axis::Max];
|
|
|
|
|
// FF effect id from EVIOCSFF, -1 if no rumble support / not active.
|
|
|
|
|
int ffEffectId = -1;
|
|
|
|
|
std::chrono::steady_clock::time_point rumbleExpiry{};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
std::unordered_map<Crafter::Gamepad::Device*, std::unique_ptr<LinuxBackend>> g_backends;
|
|
|
|
|
udev* g_udev = nullptr;
|
|
|
|
|
udev_monitor* g_monitor = nullptr;
|
|
|
|
|
bool g_initialized = false;
|
|
|
|
|
|
|
|
|
|
std::uint32_t SyspathToId(const char* syspath) {
|
|
|
|
|
// FNV-1a 32-bit, stable across runs for the same physical device.
|
|
|
|
|
std::uint32_t h = 0x811C9DC5u;
|
|
|
|
|
for (const char* p = syspath; *p; ++p) {
|
|
|
|
|
h ^= static_cast<std::uint8_t>(*p);
|
|
|
|
|
h *= 0x01000193u;
|
|
|
|
|
}
|
|
|
|
|
// Reserve 0 as "unmapped".
|
|
|
|
|
return h == 0 ? 1 : h;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BTN_* → Gamepad::Button. -1 = not a button we care about.
|
|
|
|
|
int ButtonFromKernel(int code) {
|
|
|
|
|
using B = Crafter::Gamepad::Button;
|
|
|
|
|
switch (code) {
|
|
|
|
|
case BTN_SOUTH: return (int)B::South;
|
|
|
|
|
case BTN_EAST: return (int)B::East;
|
|
|
|
|
case BTN_NORTH: return (int)B::North;
|
|
|
|
|
case BTN_WEST: return (int)B::West;
|
|
|
|
|
case BTN_TL: return (int)B::LeftBumper;
|
|
|
|
|
case BTN_TR: return (int)B::RightBumper;
|
|
|
|
|
case BTN_TL2: return (int)B::LeftTrigger;
|
|
|
|
|
case BTN_TR2: return (int)B::RightTrigger;
|
|
|
|
|
case BTN_SELECT: return (int)B::Select;
|
|
|
|
|
case BTN_START: return (int)B::Start;
|
|
|
|
|
case BTN_MODE: return (int)B::Home;
|
|
|
|
|
case BTN_THUMBL: return (int)B::LeftStickClick;
|
|
|
|
|
case BTN_THUMBR: return (int)B::RightStickClick;
|
|
|
|
|
case BTN_DPAD_UP: return (int)B::DPadUp;
|
|
|
|
|
case BTN_DPAD_DOWN: return (int)B::DPadDown;
|
|
|
|
|
case BTN_DPAD_LEFT: return (int)B::DPadLeft;
|
|
|
|
|
case BTN_DPAD_RIGHT: return (int)B::DPadRight;
|
|
|
|
|
default: return -1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Populate `bp->axisInfo` from libevdev. Sticks use ABS_X/Y/RX/RY;
|
|
|
|
|
// triggers prefer ABS_Z/RZ but fall back to ABS_BRAKE/GAS.
|
|
|
|
|
void DiscoverAxes(LinuxBackend* bp) {
|
|
|
|
|
using A = Crafter::Gamepad::Axis;
|
|
|
|
|
auto setup = [&](A which, int code, bool invertY = false) {
|
|
|
|
|
if (!libevdev_has_event_code(bp->evdev, EV_ABS, code)) return;
|
|
|
|
|
auto* info = libevdev_get_abs_info(bp->evdev, code);
|
|
|
|
|
if (!info) return;
|
|
|
|
|
auto& slot = bp->axisInfo[(std::size_t)which];
|
|
|
|
|
slot.code = code;
|
|
|
|
|
slot.min = info->minimum;
|
|
|
|
|
slot.max = info->maximum;
|
|
|
|
|
slot.flat = (float)info->flat;
|
|
|
|
|
slot.invertY = invertY;
|
|
|
|
|
};
|
|
|
|
|
setup(A::LeftStickX, ABS_X);
|
|
|
|
|
setup(A::LeftStickY, ABS_Y, /*invertY=*/true);
|
|
|
|
|
setup(A::RightStickX, ABS_RX);
|
|
|
|
|
setup(A::RightStickY, ABS_RY, /*invertY=*/true);
|
|
|
|
|
// Trigger axis: Z/RZ on Xbox, BRAKE/GAS on some others.
|
|
|
|
|
if (libevdev_has_event_code(bp->evdev, EV_ABS, ABS_Z)) {
|
|
|
|
|
setup(A::LeftTrigger, ABS_Z);
|
|
|
|
|
} else if (libevdev_has_event_code(bp->evdev, EV_ABS, ABS_BRAKE)) {
|
|
|
|
|
setup(A::LeftTrigger, ABS_BRAKE);
|
|
|
|
|
}
|
|
|
|
|
if (libevdev_has_event_code(bp->evdev, EV_ABS, ABS_RZ)) {
|
|
|
|
|
setup(A::RightTrigger, ABS_RZ);
|
|
|
|
|
} else if (libevdev_has_event_code(bp->evdev, EV_ABS, ABS_GAS)) {
|
|
|
|
|
setup(A::RightTrigger, ABS_GAS);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Normalize a raw axis value to -1..1 (sticks) or 0..1 (triggers).
|
|
|
|
|
float NormalizeAxis(const LinuxBackend::AxisInfo& info, int raw, bool isTrigger) {
|
|
|
|
|
if (info.code < 0 || info.max == info.min) return 0.0f;
|
|
|
|
|
float v;
|
|
|
|
|
if (isTrigger) {
|
|
|
|
|
v = float(raw - info.min) / float(info.max - info.min);
|
|
|
|
|
if (v < 0.0f) v = 0.0f;
|
|
|
|
|
if (v > 1.0f) v = 1.0f;
|
|
|
|
|
} else {
|
|
|
|
|
float mid = 0.5f * (info.max + info.min);
|
|
|
|
|
float halfR = 0.5f * (info.max - info.min);
|
|
|
|
|
v = (raw - mid) / halfR;
|
|
|
|
|
if (v < -1.0f) v = -1.0f;
|
|
|
|
|
if (v > 1.0f) v = 1.0f;
|
|
|
|
|
if (info.invertY) v = -v;
|
|
|
|
|
}
|
|
|
|
|
return v;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool IsGamepadDevice(libevdev* evdev) {
|
|
|
|
|
// A real gamepad reports BTN_GAMEPAD (a kernel alias for BTN_SOUTH
|
|
|
|
|
// when the device is classified as a gamepad). This filters out
|
|
|
|
|
// keyboards/mice/touchpads/tablets that share the evdev interface.
|
|
|
|
|
return libevdev_has_event_type(evdev, EV_KEY)
|
|
|
|
|
&& libevdev_has_event_code(evdev, EV_KEY, BTN_GAMEPAD);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Crafter::Gamepad::Device* OpenDevice(const char* devnode, const char* syspath, const char* name) {
|
|
|
|
|
int fd = ::open(devnode, O_RDWR | O_NONBLOCK);
|
|
|
|
|
if (fd < 0) {
|
|
|
|
|
// Read-only is fine if we just can't rumble.
|
|
|
|
|
fd = ::open(devnode, O_RDONLY | O_NONBLOCK);
|
|
|
|
|
if (fd < 0) return nullptr;
|
|
|
|
|
}
|
|
|
|
|
libevdev* evdev = nullptr;
|
|
|
|
|
if (libevdev_new_from_fd(fd, &evdev) < 0) {
|
|
|
|
|
::close(fd);
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
if (!IsGamepadDevice(evdev)) {
|
|
|
|
|
libevdev_free(evdev);
|
|
|
|
|
::close(fd);
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto dev = std::make_unique<Crafter::Gamepad::Device>();
|
|
|
|
|
dev->id = SyspathToId(syspath);
|
|
|
|
|
dev->name = name ? name : libevdev_get_name(evdev);
|
|
|
|
|
|
|
|
|
|
auto bp = std::make_unique<LinuxBackend>();
|
|
|
|
|
bp->evdev = evdev;
|
|
|
|
|
bp->fd = fd;
|
|
|
|
|
DiscoverAxes(bp.get());
|
|
|
|
|
|
|
|
|
|
auto* raw = dev.get();
|
|
|
|
|
g_backends[raw] = std::move(bp);
|
|
|
|
|
Crafter::Gamepad::connected.push_back(std::move(dev));
|
|
|
|
|
return raw;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CloseDevice(Crafter::Gamepad::Device* dev) {
|
|
|
|
|
auto it = g_backends.find(dev);
|
|
|
|
|
if (it != g_backends.end()) {
|
|
|
|
|
if (it->second->evdev) libevdev_free(it->second->evdev);
|
|
|
|
|
if (it->second->fd >= 0) ::close(it->second->fd);
|
|
|
|
|
g_backends.erase(it);
|
|
|
|
|
}
|
|
|
|
|
auto& v = Crafter::Gamepad::connected;
|
|
|
|
|
for (auto vi = v.begin(); vi != v.end(); ++vi) {
|
|
|
|
|
if (vi->get() == dev) { v.erase(vi); break; }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void EnumerateExisting() {
|
|
|
|
|
udev_enumerate* en = udev_enumerate_new(g_udev);
|
|
|
|
|
if (!en) return;
|
|
|
|
|
udev_enumerate_add_match_subsystem(en, "input");
|
|
|
|
|
udev_enumerate_scan_devices(en);
|
|
|
|
|
udev_list_entry* list = udev_enumerate_get_list_entry(en);
|
|
|
|
|
udev_list_entry* entry;
|
|
|
|
|
udev_list_entry_foreach(entry, list) {
|
|
|
|
|
const char* syspath = udev_list_entry_get_name(entry);
|
|
|
|
|
udev_device* d = udev_device_new_from_syspath(g_udev, syspath);
|
|
|
|
|
if (!d) continue;
|
|
|
|
|
const char* devnode = udev_device_get_devnode(d);
|
|
|
|
|
const char* sysname = udev_device_get_sysname(d);
|
|
|
|
|
if (devnode && sysname && std::strncmp(sysname, "event", 5) == 0) {
|
|
|
|
|
const char* name = udev_device_get_sysattr_value(
|
|
|
|
|
udev_device_get_parent_with_subsystem_devtype(d, "input", nullptr),
|
|
|
|
|
"name");
|
|
|
|
|
auto* dev = OpenDevice(devnode, syspath, name);
|
|
|
|
|
if (dev) Crafter::Gamepad::onConnected.Invoke(dev);
|
|
|
|
|
}
|
|
|
|
|
udev_device_unref(d);
|
|
|
|
|
}
|
|
|
|
|
udev_enumerate_unref(en);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void HandleHotplugEvent(udev_device* d) {
|
|
|
|
|
const char* action = udev_device_get_action(d);
|
|
|
|
|
const char* devnode = udev_device_get_devnode(d);
|
|
|
|
|
const char* sysname = udev_device_get_sysname(d);
|
|
|
|
|
const char* syspath = udev_device_get_syspath(d);
|
|
|
|
|
if (!action || !devnode || !sysname || !syspath) return;
|
|
|
|
|
if (std::strncmp(sysname, "event", 5) != 0) return;
|
|
|
|
|
|
|
|
|
|
if (std::strcmp(action, "add") == 0) {
|
|
|
|
|
const char* name = udev_device_get_sysattr_value(
|
|
|
|
|
udev_device_get_parent_with_subsystem_devtype(d, "input", nullptr),
|
|
|
|
|
"name");
|
|
|
|
|
auto* dev = OpenDevice(devnode, syspath, name);
|
|
|
|
|
if (dev) Crafter::Gamepad::onConnected.Invoke(dev);
|
|
|
|
|
} else if (std::strcmp(action, "remove") == 0) {
|
|
|
|
|
std::uint32_t id = SyspathToId(syspath);
|
|
|
|
|
for (auto& up : Crafter::Gamepad::connected) {
|
|
|
|
|
if (up->id == id) {
|
|
|
|
|
Crafter::Gamepad::Device* dev = up.get();
|
|
|
|
|
Crafter::Gamepad::onDisconnected.Invoke(dev);
|
|
|
|
|
CloseDevice(dev);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void DrainHotplug() {
|
|
|
|
|
if (!g_monitor) return;
|
|
|
|
|
for (;;) {
|
|
|
|
|
udev_device* d = udev_monitor_receive_device(g_monitor);
|
|
|
|
|
if (!d) break;
|
|
|
|
|
HandleHotplugEvent(d);
|
|
|
|
|
udev_device_unref(d);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void DrainDeviceEvents(Crafter::Gamepad::Device* dev, LinuxBackend* bp) {
|
|
|
|
|
using A = Crafter::Gamepad::Axis;
|
|
|
|
|
using B = Crafter::Gamepad::Button;
|
|
|
|
|
// Walk libevdev's internal queue; SYNC means we missed events and
|
|
|
|
|
// need to resync the state. We treat the synced events the same
|
|
|
|
|
// way as normal events — net effect is the polled state ends up
|
|
|
|
|
// correct and the event stream reports the deltas.
|
|
|
|
|
input_event ev;
|
|
|
|
|
int rc;
|
|
|
|
|
while ((rc = libevdev_next_event(bp->evdev,
|
|
|
|
|
LIBEVDEV_READ_FLAG_NORMAL | LIBEVDEV_READ_FLAG_SYNC,
|
|
|
|
|
&ev)) >= 0)
|
|
|
|
|
{
|
|
|
|
|
if (rc == LIBEVDEV_READ_STATUS_SYNC) continue; // handled in next iter
|
|
|
|
|
if (ev.type == EV_KEY) {
|
|
|
|
|
int b = ButtonFromKernel(ev.code);
|
|
|
|
|
if (b < 0) continue;
|
|
|
|
|
bool pressed = (ev.value != 0);
|
|
|
|
|
if (dev->buttons[b] == pressed) continue;
|
|
|
|
|
dev->buttons[b] = pressed;
|
|
|
|
|
if (pressed) dev->onButtonDown.Invoke((B)b);
|
|
|
|
|
else dev->onButtonUp .Invoke((B)b);
|
|
|
|
|
} else if (ev.type == EV_ABS) {
|
|
|
|
|
// ABS_HAT0X / ABS_HAT0Y emulate the D-pad on some
|
|
|
|
|
// controllers. Translate to the four DPad buttons so
|
|
|
|
|
// bindings can be uniform.
|
|
|
|
|
if (ev.code == ABS_HAT0X) {
|
|
|
|
|
bool left = ev.value < 0;
|
|
|
|
|
bool right = ev.value > 0;
|
|
|
|
|
auto edge = [&](int idx, bool now) {
|
|
|
|
|
if (dev->buttons[idx] == now) return;
|
|
|
|
|
dev->buttons[idx] = now;
|
|
|
|
|
if (now) dev->onButtonDown.Invoke((B)idx);
|
|
|
|
|
else dev->onButtonUp .Invoke((B)idx);
|
|
|
|
|
};
|
|
|
|
|
edge((int)B::DPadLeft, left);
|
|
|
|
|
edge((int)B::DPadRight, right);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (ev.code == ABS_HAT0Y) {
|
|
|
|
|
bool up = ev.value < 0;
|
|
|
|
|
bool down = ev.value > 0;
|
|
|
|
|
auto edge = [&](int idx, bool now) {
|
|
|
|
|
if (dev->buttons[idx] == now) return;
|
|
|
|
|
dev->buttons[idx] = now;
|
|
|
|
|
if (now) dev->onButtonDown.Invoke((B)idx);
|
|
|
|
|
else dev->onButtonUp .Invoke((B)idx);
|
|
|
|
|
};
|
|
|
|
|
edge((int)B::DPadUp, up);
|
|
|
|
|
edge((int)B::DPadDown, down);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
// Look up which Gamepad::Axis this kernel code maps to
|
|
|
|
|
// (by scanning the calibration table — sticks first,
|
|
|
|
|
// triggers second).
|
|
|
|
|
for (std::size_t i = 0; i < (std::size_t)A::Max; ++i) {
|
|
|
|
|
if (bp->axisInfo[i].code != ev.code) continue;
|
|
|
|
|
bool isTrigger = (i == (std::size_t)A::LeftTrigger
|
|
|
|
|
|| i == (std::size_t)A::RightTrigger);
|
|
|
|
|
float v = NormalizeAxis(bp->axisInfo[i], ev.value, isTrigger);
|
|
|
|
|
if (dev->axes[i] == v) break;
|
|
|
|
|
dev->axes[i] = v;
|
|
|
|
|
dev->onAxisChanged.Invoke((A)i);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ExpireRumbles() {
|
|
|
|
|
auto now = std::chrono::steady_clock::now();
|
|
|
|
|
for (auto& [dev, bp] : g_backends) {
|
|
|
|
|
if (bp->ffEffectId >= 0 && now >= bp->rumbleExpiry) {
|
|
|
|
|
input_event stop{};
|
|
|
|
|
stop.type = EV_FF;
|
|
|
|
|
stop.code = bp->ffEffectId;
|
|
|
|
|
stop.value = 0;
|
|
|
|
|
if (bp->fd >= 0) ::write(bp->fd, &stop, sizeof(stop));
|
|
|
|
|
ioctl(bp->fd, EVIOCRMFF, bp->ffEffectId);
|
|
|
|
|
bp->ffEffectId = -1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void EnsureInit() {
|
|
|
|
|
if (g_initialized) return;
|
|
|
|
|
g_initialized = true;
|
|
|
|
|
g_udev = udev_new();
|
|
|
|
|
if (!g_udev) return;
|
|
|
|
|
g_monitor = udev_monitor_new_from_netlink(g_udev, "udev");
|
|
|
|
|
if (g_monitor) {
|
|
|
|
|
udev_monitor_filter_add_match_subsystem_devtype(g_monitor, "input", nullptr);
|
|
|
|
|
udev_monitor_enable_receiving(g_monitor);
|
|
|
|
|
}
|
|
|
|
|
EnumerateExisting();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Crafter::Gamepad::Tick() {
|
|
|
|
|
EnsureInit();
|
|
|
|
|
DrainHotplug();
|
|
|
|
|
for (auto& up : Gamepad::connected) {
|
|
|
|
|
Device* dev = up.get();
|
|
|
|
|
auto it = g_backends.find(dev);
|
|
|
|
|
if (it == g_backends.end()) continue;
|
|
|
|
|
DrainDeviceEvents(dev, it->second.get());
|
|
|
|
|
}
|
|
|
|
|
ExpireRumbles();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Crafter::Gamepad::Rumble(Device& dev, float low, float high,
|
|
|
|
|
std::chrono::milliseconds duration)
|
|
|
|
|
{
|
|
|
|
|
auto it = g_backends.find(&dev);
|
|
|
|
|
if (it == g_backends.end()) return;
|
|
|
|
|
LinuxBackend* bp = it->second.get();
|
|
|
|
|
if (bp->fd < 0) return;
|
|
|
|
|
// Cancel any active effect first.
|
|
|
|
|
if (bp->ffEffectId >= 0) {
|
|
|
|
|
input_event stop{};
|
|
|
|
|
stop.type = EV_FF; stop.code = bp->ffEffectId; stop.value = 0;
|
|
|
|
|
::write(bp->fd, &stop, sizeof(stop));
|
|
|
|
|
ioctl(bp->fd, EVIOCRMFF, bp->ffEffectId);
|
|
|
|
|
bp->ffEffectId = -1;
|
|
|
|
|
}
|
|
|
|
|
if (duration.count() <= 0 || (low <= 0.0f && high <= 0.0f)) return;
|
|
|
|
|
auto clamp01 = [](float v) { return v < 0 ? 0.0f : v > 1.0f ? 1.0f : v; };
|
|
|
|
|
ff_effect effect{};
|
|
|
|
|
effect.type = FF_RUMBLE;
|
|
|
|
|
effect.id = -1;
|
|
|
|
|
effect.u.rumble.strong_magnitude = (std::uint16_t)(clamp01(low) * 0xFFFFu);
|
|
|
|
|
effect.u.rumble.weak_magnitude = (std::uint16_t)(clamp01(high) * 0xFFFFu);
|
|
|
|
|
effect.replay.length = (std::uint16_t)std::min<std::int64_t>(duration.count(), 0xFFFF);
|
|
|
|
|
if (ioctl(bp->fd, EVIOCSFF, &effect) < 0) return;
|
|
|
|
|
bp->ffEffectId = effect.id;
|
|
|
|
|
bp->rumbleExpiry = std::chrono::steady_clock::now() + duration;
|
|
|
|
|
input_event play{};
|
|
|
|
|
play.type = EV_FF;
|
|
|
|
|
play.code = effect.id;
|
|
|
|
|
play.value = 1;
|
|
|
|
|
::write(bp->fd, &play, sizeof(play));
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
// ────────────────────────────────────────────────────────────────────
|
feat: windows on their own clock, deferred deletion for every handle owner, staged re-uploads, and RTPass push data
Window
- updateInterval runs Update on the window's own clock while the
compositor sends no frame callbacks (hidden, minimized): the Wayland loop
is a poll() loop, and the Win32 loop sleeps until the next update. Both
read the interval each pass.
- SetPresentMode (FIFO, Mailbox, Immediate). A mode the surface lacks, or
whose swapchain has other than numFrames images (Mesa's Mailbox has four),
falls back to FIFO until the mode is set again.
- AddPollFd / RemovePollFd wake the Wayland loop on the app's descriptors.
- The swapchain has TRANSFER_DST where the surface allows it.
- CaptureNextFrame replaces SaveFrame: it records the copy in the frame,
before the present, where SaveFrame read an image already presented. A
frame unwound past before the write still frees the readback buffer.
- Inter-pass barriers on the swapchain image carry the access masks of the
stages the passes use.
Deferred deletion (issue #101)
- VulkanBuffer, Mesh, VulkanShader, ComputeShader and PipelineRTVulkan hand
their handles to the deletion queue from their destructors and moves, as
a frame in flight may still use them; a static owner goes after the
driver has torn down. VulkanShader and PipelineRTVulkan move and can't be
copied; re-Load and re-Init queue the old pipeline.
- The queue and its mutex are never destroyed, and the queue takes handles
from any thread; Device::frameCounter is atomic. Both the queue and the
staging rings count frames as one window renders them (documented).
- DrainDeletions also lets every buffer's staging ring start over: without
a Window nothing else reclaims either, so an app without one calls it
after waiting for its work.
Meshes and buffers
- A Mesh rebuild queues the old acceleration structure and builds into the
same storage after a barrier, so the BLAS address stays put when it fits.
The refit and rebuild barriers cover compute ray queries too.
- RTBuildOptions::noDuplicateAnyHit. Mesh::geometryFlags replaces opaque,
and the refit and rebuild fallbacks keep the flags.
- A re-upload into a buffer a frame in flight may read (UploadDeviceLocal
of the same size, UploadDeviceLocalRange) copies on the GPU, after those
reads, through a per-frame staging ring; on ReBAR it used to write the
buffer in place. Empty uploads record nothing.
- A compressed Mesh::Build that keeps its buffers decompresses into them
after the reads of frames in flight, and the hit shaders and ray queries
wait for the decompress as well as the BLAS build. Its vertex buffer
takes a ranged refit's copies.
- Mesh move-assigns (queueing the BLAS it replaces), so it can be replaced
in place or erased from a vector.
- VulkanBuffer::Create asks for a device address only for buffers with
SHADER_DEVICE_ADDRESS usage, as vkGetBufferDeviceAddress requires; the
others have address 0. UIRenderer::RegisterBuffer refuses those.
- Build inputs are synchronized as shader reads.
Other
- RTPass pushes the app's own push data and traces at an explicit size.
- DescriptorHeapVulkan hands freed slot ranges out again (they leaked), and
keeps its free lists sorted.
- ImageVulkan's mip chain clamps each level to 1 texel: a non-square
image's shorter side went to 0.
- VulkanShader throws on a missing file instead of failing in bad_alloc.
- CRAFTER_GRAPHICS_DEVICE picks the GPU by its number among the distinct
GPUs Vulkan lists; anything but such a number is an error.
- The debug messenger prints info messages only with
CRAFTER_GRAPHICS_VALIDATION set.
- Windows: the WinRT gamepad code moved to a C file (mingw-w64's WinRT
headers don't compile as C++), -lcombase dropped, a reference leak fixed.
- project.cpp honours --local for Crafter.Event, Math and Asset, as Asset
does for Math, and builds every test through one helper, projectArgs
included.
- Tests: DebugMessengerSeverity, DeletionQueueThreads, DeviceSelection,
HeapSlotReuse, MeshLifetime, MipChainNonSquare, PipelineLifetime,
RTPassPushData, SwapchainTransferDst, UploadInFlight, VulkanBufferAddress,
VulkanShaderLifetime, WindowUpdateClock; others extended.
Breaking: SaveFrame is CaptureNextFrame; Mesh::opaque is geometryFlags; a
buffer without SHADER_DEVICE_ADDRESS usage has no address; frameCounter is
std::atomic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:54 +02:00
|
|
|
// Win32 backend (Windows.Gaming.Input, through Crafter.Graphics-GamepadWin32.c)
|
2026-05-12 00:24:48 +02:00
|
|
|
// ────────────────────────────────────────────────────────────────────
|
|
|
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
|
|
|
|
namespace {
|
feat: windows on their own clock, deferred deletion for every handle owner, staged re-uploads, and RTPass push data
Window
- updateInterval runs Update on the window's own clock while the
compositor sends no frame callbacks (hidden, minimized): the Wayland loop
is a poll() loop, and the Win32 loop sleeps until the next update. Both
read the interval each pass.
- SetPresentMode (FIFO, Mailbox, Immediate). A mode the surface lacks, or
whose swapchain has other than numFrames images (Mesa's Mailbox has four),
falls back to FIFO until the mode is set again.
- AddPollFd / RemovePollFd wake the Wayland loop on the app's descriptors.
- The swapchain has TRANSFER_DST where the surface allows it.
- CaptureNextFrame replaces SaveFrame: it records the copy in the frame,
before the present, where SaveFrame read an image already presented. A
frame unwound past before the write still frees the readback buffer.
- Inter-pass barriers on the swapchain image carry the access masks of the
stages the passes use.
Deferred deletion (issue #101)
- VulkanBuffer, Mesh, VulkanShader, ComputeShader and PipelineRTVulkan hand
their handles to the deletion queue from their destructors and moves, as
a frame in flight may still use them; a static owner goes after the
driver has torn down. VulkanShader and PipelineRTVulkan move and can't be
copied; re-Load and re-Init queue the old pipeline.
- The queue and its mutex are never destroyed, and the queue takes handles
from any thread; Device::frameCounter is atomic. Both the queue and the
staging rings count frames as one window renders them (documented).
- DrainDeletions also lets every buffer's staging ring start over: without
a Window nothing else reclaims either, so an app without one calls it
after waiting for its work.
Meshes and buffers
- A Mesh rebuild queues the old acceleration structure and builds into the
same storage after a barrier, so the BLAS address stays put when it fits.
The refit and rebuild barriers cover compute ray queries too.
- RTBuildOptions::noDuplicateAnyHit. Mesh::geometryFlags replaces opaque,
and the refit and rebuild fallbacks keep the flags.
- A re-upload into a buffer a frame in flight may read (UploadDeviceLocal
of the same size, UploadDeviceLocalRange) copies on the GPU, after those
reads, through a per-frame staging ring; on ReBAR it used to write the
buffer in place. Empty uploads record nothing.
- A compressed Mesh::Build that keeps its buffers decompresses into them
after the reads of frames in flight, and the hit shaders and ray queries
wait for the decompress as well as the BLAS build. Its vertex buffer
takes a ranged refit's copies.
- Mesh move-assigns (queueing the BLAS it replaces), so it can be replaced
in place or erased from a vector.
- VulkanBuffer::Create asks for a device address only for buffers with
SHADER_DEVICE_ADDRESS usage, as vkGetBufferDeviceAddress requires; the
others have address 0. UIRenderer::RegisterBuffer refuses those.
- Build inputs are synchronized as shader reads.
Other
- RTPass pushes the app's own push data and traces at an explicit size.
- DescriptorHeapVulkan hands freed slot ranges out again (they leaked), and
keeps its free lists sorted.
- ImageVulkan's mip chain clamps each level to 1 texel: a non-square
image's shorter side went to 0.
- VulkanShader throws on a missing file instead of failing in bad_alloc.
- CRAFTER_GRAPHICS_DEVICE picks the GPU by its number among the distinct
GPUs Vulkan lists; anything but such a number is an error.
- The debug messenger prints info messages only with
CRAFTER_GRAPHICS_VALIDATION set.
- Windows: the WinRT gamepad code moved to a C file (mingw-w64's WinRT
headers don't compile as C++), -lcombase dropped, a reference leak fixed.
- project.cpp honours --local for Crafter.Event, Math and Asset, as Asset
does for Math, and builds every test through one helper, projectArgs
included.
- Tests: DebugMessengerSeverity, DeletionQueueThreads, DeviceSelection,
HeapSlotReuse, MeshLifetime, MipChainNonSquare, PipelineLifetime,
RTPassPushData, SwapchainTransferDst, UploadInFlight, VulkanBufferAddress,
VulkanShaderLifetime, WindowUpdateClock; others extended.
Breaking: SaveFrame is CaptureNextFrame; Mesh::opaque is geometryFlags; a
buffer without SHADER_DEVICE_ADDRESS usage has no address; frameCounter is
std::atomic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:54 +02:00
|
|
|
// Maps a connected gamepad to its Crafter::Gamepad::Device. The pointer
|
|
|
|
|
// is stable for as long as the gamepad stays connected.
|
|
|
|
|
std::unordered_map<CrafterWgiGamepad*, Crafter::Gamepad::Device*> g_byGamepad;
|
2026-05-12 00:24:48 +02:00
|
|
|
|
feat: windows on their own clock, deferred deletion for every handle owner, staged re-uploads, and RTPass push data
Window
- updateInterval runs Update on the window's own clock while the
compositor sends no frame callbacks (hidden, minimized): the Wayland loop
is a poll() loop, and the Win32 loop sleeps until the next update. Both
read the interval each pass.
- SetPresentMode (FIFO, Mailbox, Immediate). A mode the surface lacks, or
whose swapchain has other than numFrames images (Mesa's Mailbox has four),
falls back to FIFO until the mode is set again.
- AddPollFd / RemovePollFd wake the Wayland loop on the app's descriptors.
- The swapchain has TRANSFER_DST where the surface allows it.
- CaptureNextFrame replaces SaveFrame: it records the copy in the frame,
before the present, where SaveFrame read an image already presented. A
frame unwound past before the write still frees the readback buffer.
- Inter-pass barriers on the swapchain image carry the access masks of the
stages the passes use.
Deferred deletion (issue #101)
- VulkanBuffer, Mesh, VulkanShader, ComputeShader and PipelineRTVulkan hand
their handles to the deletion queue from their destructors and moves, as
a frame in flight may still use them; a static owner goes after the
driver has torn down. VulkanShader and PipelineRTVulkan move and can't be
copied; re-Load and re-Init queue the old pipeline.
- The queue and its mutex are never destroyed, and the queue takes handles
from any thread; Device::frameCounter is atomic. Both the queue and the
staging rings count frames as one window renders them (documented).
- DrainDeletions also lets every buffer's staging ring start over: without
a Window nothing else reclaims either, so an app without one calls it
after waiting for its work.
Meshes and buffers
- A Mesh rebuild queues the old acceleration structure and builds into the
same storage after a barrier, so the BLAS address stays put when it fits.
The refit and rebuild barriers cover compute ray queries too.
- RTBuildOptions::noDuplicateAnyHit. Mesh::geometryFlags replaces opaque,
and the refit and rebuild fallbacks keep the flags.
- A re-upload into a buffer a frame in flight may read (UploadDeviceLocal
of the same size, UploadDeviceLocalRange) copies on the GPU, after those
reads, through a per-frame staging ring; on ReBAR it used to write the
buffer in place. Empty uploads record nothing.
- A compressed Mesh::Build that keeps its buffers decompresses into them
after the reads of frames in flight, and the hit shaders and ray queries
wait for the decompress as well as the BLAS build. Its vertex buffer
takes a ranged refit's copies.
- Mesh move-assigns (queueing the BLAS it replaces), so it can be replaced
in place or erased from a vector.
- VulkanBuffer::Create asks for a device address only for buffers with
SHADER_DEVICE_ADDRESS usage, as vkGetBufferDeviceAddress requires; the
others have address 0. UIRenderer::RegisterBuffer refuses those.
- Build inputs are synchronized as shader reads.
Other
- RTPass pushes the app's own push data and traces at an explicit size.
- DescriptorHeapVulkan hands freed slot ranges out again (they leaked), and
keeps its free lists sorted.
- ImageVulkan's mip chain clamps each level to 1 texel: a non-square
image's shorter side went to 0.
- VulkanShader throws on a missing file instead of failing in bad_alloc.
- CRAFTER_GRAPHICS_DEVICE picks the GPU by its number among the distinct
GPUs Vulkan lists; anything but such a number is an error.
- The debug messenger prints info messages only with
CRAFTER_GRAPHICS_VALIDATION set.
- Windows: the WinRT gamepad code moved to a C file (mingw-w64's WinRT
headers don't compile as C++), -lcombase dropped, a reference leak fixed.
- project.cpp honours --local for Crafter.Event, Math and Asset, as Asset
does for Math, and builds every test through one helper, projectArgs
included.
- Tests: DebugMessengerSeverity, DeletionQueueThreads, DeviceSelection,
HeapSlotReuse, MeshLifetime, MipChainNonSquare, PipelineLifetime,
RTPassPushData, SwapchainTransferDst, UploadInFlight, VulkanBufferAddress,
VulkanShaderLifetime, WindowUpdateClock; others extended.
Breaking: SaveFrame is CaptureNextFrame; Mesh::opaque is geometryFlags; a
buffer without SHADER_DEVICE_ADDRESS usage has no address; frameCounter is
std::atomic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:54 +02:00
|
|
|
std::uint32_t PointerToId(CrafterWgiGamepad* g) {
|
2026-05-12 00:24:48 +02:00
|
|
|
// Hash the pointer for a stable-while-connected id. (NonRoamableId
|
|
|
|
|
// would be more durable across reconnects but adds another COM
|
|
|
|
|
// round-trip; pointer hash is fine for v1.)
|
|
|
|
|
std::uintptr_t v = reinterpret_cast<std::uintptr_t>(g);
|
|
|
|
|
v = (v ^ (v >> 16)) * 0x85EBCA6Bu;
|
|
|
|
|
v = (v ^ (v >> 13)) * 0xC2B2AE35u;
|
|
|
|
|
v = v ^ (v >> 16);
|
|
|
|
|
std::uint32_t out = (std::uint32_t)v;
|
|
|
|
|
return out == 0 ? 1 : out;
|
|
|
|
|
}
|
|
|
|
|
|
feat: windows on their own clock, deferred deletion for every handle owner, staged re-uploads, and RTPass push data
Window
- updateInterval runs Update on the window's own clock while the
compositor sends no frame callbacks (hidden, minimized): the Wayland loop
is a poll() loop, and the Win32 loop sleeps until the next update. Both
read the interval each pass.
- SetPresentMode (FIFO, Mailbox, Immediate). A mode the surface lacks, or
whose swapchain has other than numFrames images (Mesa's Mailbox has four),
falls back to FIFO until the mode is set again.
- AddPollFd / RemovePollFd wake the Wayland loop on the app's descriptors.
- The swapchain has TRANSFER_DST where the surface allows it.
- CaptureNextFrame replaces SaveFrame: it records the copy in the frame,
before the present, where SaveFrame read an image already presented. A
frame unwound past before the write still frees the readback buffer.
- Inter-pass barriers on the swapchain image carry the access masks of the
stages the passes use.
Deferred deletion (issue #101)
- VulkanBuffer, Mesh, VulkanShader, ComputeShader and PipelineRTVulkan hand
their handles to the deletion queue from their destructors and moves, as
a frame in flight may still use them; a static owner goes after the
driver has torn down. VulkanShader and PipelineRTVulkan move and can't be
copied; re-Load and re-Init queue the old pipeline.
- The queue and its mutex are never destroyed, and the queue takes handles
from any thread; Device::frameCounter is atomic. Both the queue and the
staging rings count frames as one window renders them (documented).
- DrainDeletions also lets every buffer's staging ring start over: without
a Window nothing else reclaims either, so an app without one calls it
after waiting for its work.
Meshes and buffers
- A Mesh rebuild queues the old acceleration structure and builds into the
same storage after a barrier, so the BLAS address stays put when it fits.
The refit and rebuild barriers cover compute ray queries too.
- RTBuildOptions::noDuplicateAnyHit. Mesh::geometryFlags replaces opaque,
and the refit and rebuild fallbacks keep the flags.
- A re-upload into a buffer a frame in flight may read (UploadDeviceLocal
of the same size, UploadDeviceLocalRange) copies on the GPU, after those
reads, through a per-frame staging ring; on ReBAR it used to write the
buffer in place. Empty uploads record nothing.
- A compressed Mesh::Build that keeps its buffers decompresses into them
after the reads of frames in flight, and the hit shaders and ray queries
wait for the decompress as well as the BLAS build. Its vertex buffer
takes a ranged refit's copies.
- Mesh move-assigns (queueing the BLAS it replaces), so it can be replaced
in place or erased from a vector.
- VulkanBuffer::Create asks for a device address only for buffers with
SHADER_DEVICE_ADDRESS usage, as vkGetBufferDeviceAddress requires; the
others have address 0. UIRenderer::RegisterBuffer refuses those.
- Build inputs are synchronized as shader reads.
Other
- RTPass pushes the app's own push data and traces at an explicit size.
- DescriptorHeapVulkan hands freed slot ranges out again (they leaked), and
keeps its free lists sorted.
- ImageVulkan's mip chain clamps each level to 1 texel: a non-square
image's shorter side went to 0.
- VulkanShader throws on a missing file instead of failing in bad_alloc.
- CRAFTER_GRAPHICS_DEVICE picks the GPU by its number among the distinct
GPUs Vulkan lists; anything but such a number is an error.
- The debug messenger prints info messages only with
CRAFTER_GRAPHICS_VALIDATION set.
- Windows: the WinRT gamepad code moved to a C file (mingw-w64's WinRT
headers don't compile as C++), -lcombase dropped, a reference leak fixed.
- project.cpp honours --local for Crafter.Event, Math and Asset, as Asset
does for Math, and builds every test through one helper, projectArgs
included.
- Tests: DebugMessengerSeverity, DeletionQueueThreads, DeviceSelection,
HeapSlotReuse, MeshLifetime, MipChainNonSquare, PipelineLifetime,
RTPassPushData, SwapchainTransferDst, UploadInFlight, VulkanBufferAddress,
VulkanShaderLifetime, WindowUpdateClock; others extended.
Breaking: SaveFrame is CaptureNextFrame; Mesh::opaque is geometryFlags; a
buffer without SHADER_DEVICE_ADDRESS usage has no address; frameCounter is
std::atomic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:54 +02:00
|
|
|
void ApplyReading(Crafter::Gamepad::Device* dev, const CrafterWgiReading& r) {
|
2026-05-12 00:24:48 +02:00
|
|
|
using B = Crafter::Gamepad::Button;
|
|
|
|
|
using A = Crafter::Gamepad::Axis;
|
|
|
|
|
|
feat: windows on their own clock, deferred deletion for every handle owner, staged re-uploads, and RTPass push data
Window
- updateInterval runs Update on the window's own clock while the
compositor sends no frame callbacks (hidden, minimized): the Wayland loop
is a poll() loop, and the Win32 loop sleeps until the next update. Both
read the interval each pass.
- SetPresentMode (FIFO, Mailbox, Immediate). A mode the surface lacks, or
whose swapchain has other than numFrames images (Mesa's Mailbox has four),
falls back to FIFO until the mode is set again.
- AddPollFd / RemovePollFd wake the Wayland loop on the app's descriptors.
- The swapchain has TRANSFER_DST where the surface allows it.
- CaptureNextFrame replaces SaveFrame: it records the copy in the frame,
before the present, where SaveFrame read an image already presented. A
frame unwound past before the write still frees the readback buffer.
- Inter-pass barriers on the swapchain image carry the access masks of the
stages the passes use.
Deferred deletion (issue #101)
- VulkanBuffer, Mesh, VulkanShader, ComputeShader and PipelineRTVulkan hand
their handles to the deletion queue from their destructors and moves, as
a frame in flight may still use them; a static owner goes after the
driver has torn down. VulkanShader and PipelineRTVulkan move and can't be
copied; re-Load and re-Init queue the old pipeline.
- The queue and its mutex are never destroyed, and the queue takes handles
from any thread; Device::frameCounter is atomic. Both the queue and the
staging rings count frames as one window renders them (documented).
- DrainDeletions also lets every buffer's staging ring start over: without
a Window nothing else reclaims either, so an app without one calls it
after waiting for its work.
Meshes and buffers
- A Mesh rebuild queues the old acceleration structure and builds into the
same storage after a barrier, so the BLAS address stays put when it fits.
The refit and rebuild barriers cover compute ray queries too.
- RTBuildOptions::noDuplicateAnyHit. Mesh::geometryFlags replaces opaque,
and the refit and rebuild fallbacks keep the flags.
- A re-upload into a buffer a frame in flight may read (UploadDeviceLocal
of the same size, UploadDeviceLocalRange) copies on the GPU, after those
reads, through a per-frame staging ring; on ReBAR it used to write the
buffer in place. Empty uploads record nothing.
- A compressed Mesh::Build that keeps its buffers decompresses into them
after the reads of frames in flight, and the hit shaders and ray queries
wait for the decompress as well as the BLAS build. Its vertex buffer
takes a ranged refit's copies.
- Mesh move-assigns (queueing the BLAS it replaces), so it can be replaced
in place or erased from a vector.
- VulkanBuffer::Create asks for a device address only for buffers with
SHADER_DEVICE_ADDRESS usage, as vkGetBufferDeviceAddress requires; the
others have address 0. UIRenderer::RegisterBuffer refuses those.
- Build inputs are synchronized as shader reads.
Other
- RTPass pushes the app's own push data and traces at an explicit size.
- DescriptorHeapVulkan hands freed slot ranges out again (they leaked), and
keeps its free lists sorted.
- ImageVulkan's mip chain clamps each level to 1 texel: a non-square
image's shorter side went to 0.
- VulkanShader throws on a missing file instead of failing in bad_alloc.
- CRAFTER_GRAPHICS_DEVICE picks the GPU by its number among the distinct
GPUs Vulkan lists; anything but such a number is an error.
- The debug messenger prints info messages only with
CRAFTER_GRAPHICS_VALIDATION set.
- Windows: the WinRT gamepad code moved to a C file (mingw-w64's WinRT
headers don't compile as C++), -lcombase dropped, a reference leak fixed.
- project.cpp honours --local for Crafter.Event, Math and Asset, as Asset
does for Math, and builds every test through one helper, projectArgs
included.
- Tests: DebugMessengerSeverity, DeletionQueueThreads, DeviceSelection,
HeapSlotReuse, MeshLifetime, MipChainNonSquare, PipelineLifetime,
RTPassPushData, SwapchainTransferDst, UploadInFlight, VulkanBufferAddress,
VulkanShaderLifetime, WindowUpdateClock; others extended.
Breaking: SaveFrame is CaptureNextFrame; Mesh::opaque is geometryFlags; a
buffer without SHADER_DEVICE_ADDRESS usage has no address; frameCounter is
std::atomic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:54 +02:00
|
|
|
struct Map { B which; std::uint32_t mask; };
|
2026-05-12 00:24:48 +02:00
|
|
|
static const Map buttonMap[] = {
|
feat: windows on their own clock, deferred deletion for every handle owner, staged re-uploads, and RTPass push data
Window
- updateInterval runs Update on the window's own clock while the
compositor sends no frame callbacks (hidden, minimized): the Wayland loop
is a poll() loop, and the Win32 loop sleeps until the next update. Both
read the interval each pass.
- SetPresentMode (FIFO, Mailbox, Immediate). A mode the surface lacks, or
whose swapchain has other than numFrames images (Mesa's Mailbox has four),
falls back to FIFO until the mode is set again.
- AddPollFd / RemovePollFd wake the Wayland loop on the app's descriptors.
- The swapchain has TRANSFER_DST where the surface allows it.
- CaptureNextFrame replaces SaveFrame: it records the copy in the frame,
before the present, where SaveFrame read an image already presented. A
frame unwound past before the write still frees the readback buffer.
- Inter-pass barriers on the swapchain image carry the access masks of the
stages the passes use.
Deferred deletion (issue #101)
- VulkanBuffer, Mesh, VulkanShader, ComputeShader and PipelineRTVulkan hand
their handles to the deletion queue from their destructors and moves, as
a frame in flight may still use them; a static owner goes after the
driver has torn down. VulkanShader and PipelineRTVulkan move and can't be
copied; re-Load and re-Init queue the old pipeline.
- The queue and its mutex are never destroyed, and the queue takes handles
from any thread; Device::frameCounter is atomic. Both the queue and the
staging rings count frames as one window renders them (documented).
- DrainDeletions also lets every buffer's staging ring start over: without
a Window nothing else reclaims either, so an app without one calls it
after waiting for its work.
Meshes and buffers
- A Mesh rebuild queues the old acceleration structure and builds into the
same storage after a barrier, so the BLAS address stays put when it fits.
The refit and rebuild barriers cover compute ray queries too.
- RTBuildOptions::noDuplicateAnyHit. Mesh::geometryFlags replaces opaque,
and the refit and rebuild fallbacks keep the flags.
- A re-upload into a buffer a frame in flight may read (UploadDeviceLocal
of the same size, UploadDeviceLocalRange) copies on the GPU, after those
reads, through a per-frame staging ring; on ReBAR it used to write the
buffer in place. Empty uploads record nothing.
- A compressed Mesh::Build that keeps its buffers decompresses into them
after the reads of frames in flight, and the hit shaders and ray queries
wait for the decompress as well as the BLAS build. Its vertex buffer
takes a ranged refit's copies.
- Mesh move-assigns (queueing the BLAS it replaces), so it can be replaced
in place or erased from a vector.
- VulkanBuffer::Create asks for a device address only for buffers with
SHADER_DEVICE_ADDRESS usage, as vkGetBufferDeviceAddress requires; the
others have address 0. UIRenderer::RegisterBuffer refuses those.
- Build inputs are synchronized as shader reads.
Other
- RTPass pushes the app's own push data and traces at an explicit size.
- DescriptorHeapVulkan hands freed slot ranges out again (they leaked), and
keeps its free lists sorted.
- ImageVulkan's mip chain clamps each level to 1 texel: a non-square
image's shorter side went to 0.
- VulkanShader throws on a missing file instead of failing in bad_alloc.
- CRAFTER_GRAPHICS_DEVICE picks the GPU by its number among the distinct
GPUs Vulkan lists; anything but such a number is an error.
- The debug messenger prints info messages only with
CRAFTER_GRAPHICS_VALIDATION set.
- Windows: the WinRT gamepad code moved to a C file (mingw-w64's WinRT
headers don't compile as C++), -lcombase dropped, a reference leak fixed.
- project.cpp honours --local for Crafter.Event, Math and Asset, as Asset
does for Math, and builds every test through one helper, projectArgs
included.
- Tests: DebugMessengerSeverity, DeletionQueueThreads, DeviceSelection,
HeapSlotReuse, MeshLifetime, MipChainNonSquare, PipelineLifetime,
RTPassPushData, SwapchainTransferDst, UploadInFlight, VulkanBufferAddress,
VulkanShaderLifetime, WindowUpdateClock; others extended.
Breaking: SaveFrame is CaptureNextFrame; Mesh::opaque is geometryFlags; a
buffer without SHADER_DEVICE_ADDRESS usage has no address; frameCounter is
std::atomic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:54 +02:00
|
|
|
{ B::DPadUp, CRAFTER_WGI_BUTTON_DPAD_UP },
|
|
|
|
|
{ B::DPadDown, CRAFTER_WGI_BUTTON_DPAD_DOWN },
|
|
|
|
|
{ B::DPadLeft, CRAFTER_WGI_BUTTON_DPAD_LEFT },
|
|
|
|
|
{ B::DPadRight, CRAFTER_WGI_BUTTON_DPAD_RIGHT },
|
|
|
|
|
{ B::Start, CRAFTER_WGI_BUTTON_MENU },
|
|
|
|
|
{ B::Select, CRAFTER_WGI_BUTTON_VIEW },
|
|
|
|
|
{ B::LeftStickClick, CRAFTER_WGI_BUTTON_LEFT_THUMBSTICK },
|
|
|
|
|
{ B::RightStickClick,CRAFTER_WGI_BUTTON_RIGHT_THUMBSTICK },
|
|
|
|
|
{ B::LeftBumper, CRAFTER_WGI_BUTTON_LEFT_SHOULDER },
|
|
|
|
|
{ B::RightBumper, CRAFTER_WGI_BUTTON_RIGHT_SHOULDER },
|
|
|
|
|
{ B::South, CRAFTER_WGI_BUTTON_A },
|
|
|
|
|
{ B::East, CRAFTER_WGI_BUTTON_B },
|
|
|
|
|
{ B::West, CRAFTER_WGI_BUTTON_X },
|
|
|
|
|
{ B::North, CRAFTER_WGI_BUTTON_Y },
|
2026-05-12 00:24:48 +02:00
|
|
|
};
|
|
|
|
|
for (const Map& m : buttonMap) {
|
feat: windows on their own clock, deferred deletion for every handle owner, staged re-uploads, and RTPass push data
Window
- updateInterval runs Update on the window's own clock while the
compositor sends no frame callbacks (hidden, minimized): the Wayland loop
is a poll() loop, and the Win32 loop sleeps until the next update. Both
read the interval each pass.
- SetPresentMode (FIFO, Mailbox, Immediate). A mode the surface lacks, or
whose swapchain has other than numFrames images (Mesa's Mailbox has four),
falls back to FIFO until the mode is set again.
- AddPollFd / RemovePollFd wake the Wayland loop on the app's descriptors.
- The swapchain has TRANSFER_DST where the surface allows it.
- CaptureNextFrame replaces SaveFrame: it records the copy in the frame,
before the present, where SaveFrame read an image already presented. A
frame unwound past before the write still frees the readback buffer.
- Inter-pass barriers on the swapchain image carry the access masks of the
stages the passes use.
Deferred deletion (issue #101)
- VulkanBuffer, Mesh, VulkanShader, ComputeShader and PipelineRTVulkan hand
their handles to the deletion queue from their destructors and moves, as
a frame in flight may still use them; a static owner goes after the
driver has torn down. VulkanShader and PipelineRTVulkan move and can't be
copied; re-Load and re-Init queue the old pipeline.
- The queue and its mutex are never destroyed, and the queue takes handles
from any thread; Device::frameCounter is atomic. Both the queue and the
staging rings count frames as one window renders them (documented).
- DrainDeletions also lets every buffer's staging ring start over: without
a Window nothing else reclaims either, so an app without one calls it
after waiting for its work.
Meshes and buffers
- A Mesh rebuild queues the old acceleration structure and builds into the
same storage after a barrier, so the BLAS address stays put when it fits.
The refit and rebuild barriers cover compute ray queries too.
- RTBuildOptions::noDuplicateAnyHit. Mesh::geometryFlags replaces opaque,
and the refit and rebuild fallbacks keep the flags.
- A re-upload into a buffer a frame in flight may read (UploadDeviceLocal
of the same size, UploadDeviceLocalRange) copies on the GPU, after those
reads, through a per-frame staging ring; on ReBAR it used to write the
buffer in place. Empty uploads record nothing.
- A compressed Mesh::Build that keeps its buffers decompresses into them
after the reads of frames in flight, and the hit shaders and ray queries
wait for the decompress as well as the BLAS build. Its vertex buffer
takes a ranged refit's copies.
- Mesh move-assigns (queueing the BLAS it replaces), so it can be replaced
in place or erased from a vector.
- VulkanBuffer::Create asks for a device address only for buffers with
SHADER_DEVICE_ADDRESS usage, as vkGetBufferDeviceAddress requires; the
others have address 0. UIRenderer::RegisterBuffer refuses those.
- Build inputs are synchronized as shader reads.
Other
- RTPass pushes the app's own push data and traces at an explicit size.
- DescriptorHeapVulkan hands freed slot ranges out again (they leaked), and
keeps its free lists sorted.
- ImageVulkan's mip chain clamps each level to 1 texel: a non-square
image's shorter side went to 0.
- VulkanShader throws on a missing file instead of failing in bad_alloc.
- CRAFTER_GRAPHICS_DEVICE picks the GPU by its number among the distinct
GPUs Vulkan lists; anything but such a number is an error.
- The debug messenger prints info messages only with
CRAFTER_GRAPHICS_VALIDATION set.
- Windows: the WinRT gamepad code moved to a C file (mingw-w64's WinRT
headers don't compile as C++), -lcombase dropped, a reference leak fixed.
- project.cpp honours --local for Crafter.Event, Math and Asset, as Asset
does for Math, and builds every test through one helper, projectArgs
included.
- Tests: DebugMessengerSeverity, DeletionQueueThreads, DeviceSelection,
HeapSlotReuse, MeshLifetime, MipChainNonSquare, PipelineLifetime,
RTPassPushData, SwapchainTransferDst, UploadInFlight, VulkanBufferAddress,
VulkanShaderLifetime, WindowUpdateClock; others extended.
Breaking: SaveFrame is CaptureNextFrame; Mesh::opaque is geometryFlags; a
buffer without SHADER_DEVICE_ADDRESS usage has no address; frameCounter is
std::atomic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:54 +02:00
|
|
|
bool now = (r.buttons & m.mask) != 0;
|
2026-05-12 00:24:48 +02:00
|
|
|
bool& cur = dev->buttons[(std::size_t)m.which];
|
|
|
|
|
if (cur == now) continue;
|
|
|
|
|
cur = now;
|
|
|
|
|
if (now) dev->onButtonDown.Invoke(m.which);
|
|
|
|
|
else dev->onButtonUp .Invoke(m.which);
|
|
|
|
|
}
|
|
|
|
|
// WGI doesn't have a Home/Guide button in the standard Gamepad
|
|
|
|
|
// interface (it's reserved for the OS). Leave dev->buttons[Home]
|
|
|
|
|
// at false.
|
|
|
|
|
|
|
|
|
|
auto axis = [&](A which, float v) {
|
|
|
|
|
float& cur = dev->axes[(std::size_t)which];
|
|
|
|
|
if (cur == v) return;
|
|
|
|
|
cur = v;
|
|
|
|
|
dev->onAxisChanged.Invoke(which);
|
|
|
|
|
};
|
feat: windows on their own clock, deferred deletion for every handle owner, staged re-uploads, and RTPass push data
Window
- updateInterval runs Update on the window's own clock while the
compositor sends no frame callbacks (hidden, minimized): the Wayland loop
is a poll() loop, and the Win32 loop sleeps until the next update. Both
read the interval each pass.
- SetPresentMode (FIFO, Mailbox, Immediate). A mode the surface lacks, or
whose swapchain has other than numFrames images (Mesa's Mailbox has four),
falls back to FIFO until the mode is set again.
- AddPollFd / RemovePollFd wake the Wayland loop on the app's descriptors.
- The swapchain has TRANSFER_DST where the surface allows it.
- CaptureNextFrame replaces SaveFrame: it records the copy in the frame,
before the present, where SaveFrame read an image already presented. A
frame unwound past before the write still frees the readback buffer.
- Inter-pass barriers on the swapchain image carry the access masks of the
stages the passes use.
Deferred deletion (issue #101)
- VulkanBuffer, Mesh, VulkanShader, ComputeShader and PipelineRTVulkan hand
their handles to the deletion queue from their destructors and moves, as
a frame in flight may still use them; a static owner goes after the
driver has torn down. VulkanShader and PipelineRTVulkan move and can't be
copied; re-Load and re-Init queue the old pipeline.
- The queue and its mutex are never destroyed, and the queue takes handles
from any thread; Device::frameCounter is atomic. Both the queue and the
staging rings count frames as one window renders them (documented).
- DrainDeletions also lets every buffer's staging ring start over: without
a Window nothing else reclaims either, so an app without one calls it
after waiting for its work.
Meshes and buffers
- A Mesh rebuild queues the old acceleration structure and builds into the
same storage after a barrier, so the BLAS address stays put when it fits.
The refit and rebuild barriers cover compute ray queries too.
- RTBuildOptions::noDuplicateAnyHit. Mesh::geometryFlags replaces opaque,
and the refit and rebuild fallbacks keep the flags.
- A re-upload into a buffer a frame in flight may read (UploadDeviceLocal
of the same size, UploadDeviceLocalRange) copies on the GPU, after those
reads, through a per-frame staging ring; on ReBAR it used to write the
buffer in place. Empty uploads record nothing.
- A compressed Mesh::Build that keeps its buffers decompresses into them
after the reads of frames in flight, and the hit shaders and ray queries
wait for the decompress as well as the BLAS build. Its vertex buffer
takes a ranged refit's copies.
- Mesh move-assigns (queueing the BLAS it replaces), so it can be replaced
in place or erased from a vector.
- VulkanBuffer::Create asks for a device address only for buffers with
SHADER_DEVICE_ADDRESS usage, as vkGetBufferDeviceAddress requires; the
others have address 0. UIRenderer::RegisterBuffer refuses those.
- Build inputs are synchronized as shader reads.
Other
- RTPass pushes the app's own push data and traces at an explicit size.
- DescriptorHeapVulkan hands freed slot ranges out again (they leaked), and
keeps its free lists sorted.
- ImageVulkan's mip chain clamps each level to 1 texel: a non-square
image's shorter side went to 0.
- VulkanShader throws on a missing file instead of failing in bad_alloc.
- CRAFTER_GRAPHICS_DEVICE picks the GPU by its number among the distinct
GPUs Vulkan lists; anything but such a number is an error.
- The debug messenger prints info messages only with
CRAFTER_GRAPHICS_VALIDATION set.
- Windows: the WinRT gamepad code moved to a C file (mingw-w64's WinRT
headers don't compile as C++), -lcombase dropped, a reference leak fixed.
- project.cpp honours --local for Crafter.Event, Math and Asset, as Asset
does for Math, and builds every test through one helper, projectArgs
included.
- Tests: DebugMessengerSeverity, DeletionQueueThreads, DeviceSelection,
HeapSlotReuse, MeshLifetime, MipChainNonSquare, PipelineLifetime,
RTPassPushData, SwapchainTransferDst, UploadInFlight, VulkanBufferAddress,
VulkanShaderLifetime, WindowUpdateClock; others extended.
Breaking: SaveFrame is CaptureNextFrame; Mesh::opaque is geometryFlags; a
buffer without SHADER_DEVICE_ADDRESS usage has no address; frameCounter is
std::atomic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:54 +02:00
|
|
|
axis(A::LeftStickX, (float)r.leftStickX);
|
|
|
|
|
axis(A::LeftStickY, (float)r.leftStickY);
|
|
|
|
|
axis(A::RightStickX, (float)r.rightStickX);
|
|
|
|
|
axis(A::RightStickY, (float)r.rightStickY);
|
|
|
|
|
axis(A::LeftTrigger, (float)r.leftTrigger);
|
|
|
|
|
axis(A::RightTrigger, (float)r.rightTrigger);
|
2026-05-12 00:24:48 +02:00
|
|
|
|
|
|
|
|
// Triggers also expose digital "pressed" buttons at a threshold.
|
|
|
|
|
constexpr float kTriggerThreshold = 0.1f;
|
|
|
|
|
auto trigBtn = [&](B which, double value) {
|
|
|
|
|
bool now = value >= kTriggerThreshold;
|
|
|
|
|
bool& cur = dev->buttons[(std::size_t)which];
|
|
|
|
|
if (cur == now) return;
|
|
|
|
|
cur = now;
|
|
|
|
|
if (now) dev->onButtonDown.Invoke(which);
|
|
|
|
|
else dev->onButtonUp .Invoke(which);
|
|
|
|
|
};
|
feat: windows on their own clock, deferred deletion for every handle owner, staged re-uploads, and RTPass push data
Window
- updateInterval runs Update on the window's own clock while the
compositor sends no frame callbacks (hidden, minimized): the Wayland loop
is a poll() loop, and the Win32 loop sleeps until the next update. Both
read the interval each pass.
- SetPresentMode (FIFO, Mailbox, Immediate). A mode the surface lacks, or
whose swapchain has other than numFrames images (Mesa's Mailbox has four),
falls back to FIFO until the mode is set again.
- AddPollFd / RemovePollFd wake the Wayland loop on the app's descriptors.
- The swapchain has TRANSFER_DST where the surface allows it.
- CaptureNextFrame replaces SaveFrame: it records the copy in the frame,
before the present, where SaveFrame read an image already presented. A
frame unwound past before the write still frees the readback buffer.
- Inter-pass barriers on the swapchain image carry the access masks of the
stages the passes use.
Deferred deletion (issue #101)
- VulkanBuffer, Mesh, VulkanShader, ComputeShader and PipelineRTVulkan hand
their handles to the deletion queue from their destructors and moves, as
a frame in flight may still use them; a static owner goes after the
driver has torn down. VulkanShader and PipelineRTVulkan move and can't be
copied; re-Load and re-Init queue the old pipeline.
- The queue and its mutex are never destroyed, and the queue takes handles
from any thread; Device::frameCounter is atomic. Both the queue and the
staging rings count frames as one window renders them (documented).
- DrainDeletions also lets every buffer's staging ring start over: without
a Window nothing else reclaims either, so an app without one calls it
after waiting for its work.
Meshes and buffers
- A Mesh rebuild queues the old acceleration structure and builds into the
same storage after a barrier, so the BLAS address stays put when it fits.
The refit and rebuild barriers cover compute ray queries too.
- RTBuildOptions::noDuplicateAnyHit. Mesh::geometryFlags replaces opaque,
and the refit and rebuild fallbacks keep the flags.
- A re-upload into a buffer a frame in flight may read (UploadDeviceLocal
of the same size, UploadDeviceLocalRange) copies on the GPU, after those
reads, through a per-frame staging ring; on ReBAR it used to write the
buffer in place. Empty uploads record nothing.
- A compressed Mesh::Build that keeps its buffers decompresses into them
after the reads of frames in flight, and the hit shaders and ray queries
wait for the decompress as well as the BLAS build. Its vertex buffer
takes a ranged refit's copies.
- Mesh move-assigns (queueing the BLAS it replaces), so it can be replaced
in place or erased from a vector.
- VulkanBuffer::Create asks for a device address only for buffers with
SHADER_DEVICE_ADDRESS usage, as vkGetBufferDeviceAddress requires; the
others have address 0. UIRenderer::RegisterBuffer refuses those.
- Build inputs are synchronized as shader reads.
Other
- RTPass pushes the app's own push data and traces at an explicit size.
- DescriptorHeapVulkan hands freed slot ranges out again (they leaked), and
keeps its free lists sorted.
- ImageVulkan's mip chain clamps each level to 1 texel: a non-square
image's shorter side went to 0.
- VulkanShader throws on a missing file instead of failing in bad_alloc.
- CRAFTER_GRAPHICS_DEVICE picks the GPU by its number among the distinct
GPUs Vulkan lists; anything but such a number is an error.
- The debug messenger prints info messages only with
CRAFTER_GRAPHICS_VALIDATION set.
- Windows: the WinRT gamepad code moved to a C file (mingw-w64's WinRT
headers don't compile as C++), -lcombase dropped, a reference leak fixed.
- project.cpp honours --local for Crafter.Event, Math and Asset, as Asset
does for Math, and builds every test through one helper, projectArgs
included.
- Tests: DebugMessengerSeverity, DeletionQueueThreads, DeviceSelection,
HeapSlotReuse, MeshLifetime, MipChainNonSquare, PipelineLifetime,
RTPassPushData, SwapchainTransferDst, UploadInFlight, VulkanBufferAddress,
VulkanShaderLifetime, WindowUpdateClock; others extended.
Breaking: SaveFrame is CaptureNextFrame; Mesh::opaque is geometryFlags; a
buffer without SHADER_DEVICE_ADDRESS usage has no address; frameCounter is
std::atomic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:54 +02:00
|
|
|
trigBtn(B::LeftTrigger, r.leftTrigger);
|
|
|
|
|
trigBtn(B::RightTrigger, r.rightTrigger);
|
2026-05-12 00:24:48 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Crafter::Gamepad::Tick() {
|
feat: windows on their own clock, deferred deletion for every handle owner, staged re-uploads, and RTPass push data
Window
- updateInterval runs Update on the window's own clock while the
compositor sends no frame callbacks (hidden, minimized): the Wayland loop
is a poll() loop, and the Win32 loop sleeps until the next update. Both
read the interval each pass.
- SetPresentMode (FIFO, Mailbox, Immediate). A mode the surface lacks, or
whose swapchain has other than numFrames images (Mesa's Mailbox has four),
falls back to FIFO until the mode is set again.
- AddPollFd / RemovePollFd wake the Wayland loop on the app's descriptors.
- The swapchain has TRANSFER_DST where the surface allows it.
- CaptureNextFrame replaces SaveFrame: it records the copy in the frame,
before the present, where SaveFrame read an image already presented. A
frame unwound past before the write still frees the readback buffer.
- Inter-pass barriers on the swapchain image carry the access masks of the
stages the passes use.
Deferred deletion (issue #101)
- VulkanBuffer, Mesh, VulkanShader, ComputeShader and PipelineRTVulkan hand
their handles to the deletion queue from their destructors and moves, as
a frame in flight may still use them; a static owner goes after the
driver has torn down. VulkanShader and PipelineRTVulkan move and can't be
copied; re-Load and re-Init queue the old pipeline.
- The queue and its mutex are never destroyed, and the queue takes handles
from any thread; Device::frameCounter is atomic. Both the queue and the
staging rings count frames as one window renders them (documented).
- DrainDeletions also lets every buffer's staging ring start over: without
a Window nothing else reclaims either, so an app without one calls it
after waiting for its work.
Meshes and buffers
- A Mesh rebuild queues the old acceleration structure and builds into the
same storage after a barrier, so the BLAS address stays put when it fits.
The refit and rebuild barriers cover compute ray queries too.
- RTBuildOptions::noDuplicateAnyHit. Mesh::geometryFlags replaces opaque,
and the refit and rebuild fallbacks keep the flags.
- A re-upload into a buffer a frame in flight may read (UploadDeviceLocal
of the same size, UploadDeviceLocalRange) copies on the GPU, after those
reads, through a per-frame staging ring; on ReBAR it used to write the
buffer in place. Empty uploads record nothing.
- A compressed Mesh::Build that keeps its buffers decompresses into them
after the reads of frames in flight, and the hit shaders and ray queries
wait for the decompress as well as the BLAS build. Its vertex buffer
takes a ranged refit's copies.
- Mesh move-assigns (queueing the BLAS it replaces), so it can be replaced
in place or erased from a vector.
- VulkanBuffer::Create asks for a device address only for buffers with
SHADER_DEVICE_ADDRESS usage, as vkGetBufferDeviceAddress requires; the
others have address 0. UIRenderer::RegisterBuffer refuses those.
- Build inputs are synchronized as shader reads.
Other
- RTPass pushes the app's own push data and traces at an explicit size.
- DescriptorHeapVulkan hands freed slot ranges out again (they leaked), and
keeps its free lists sorted.
- ImageVulkan's mip chain clamps each level to 1 texel: a non-square
image's shorter side went to 0.
- VulkanShader throws on a missing file instead of failing in bad_alloc.
- CRAFTER_GRAPHICS_DEVICE picks the GPU by its number among the distinct
GPUs Vulkan lists; anything but such a number is an error.
- The debug messenger prints info messages only with
CRAFTER_GRAPHICS_VALIDATION set.
- Windows: the WinRT gamepad code moved to a C file (mingw-w64's WinRT
headers don't compile as C++), -lcombase dropped, a reference leak fixed.
- project.cpp honours --local for Crafter.Event, Math and Asset, as Asset
does for Math, and builds every test through one helper, projectArgs
included.
- Tests: DebugMessengerSeverity, DeletionQueueThreads, DeviceSelection,
HeapSlotReuse, MeshLifetime, MipChainNonSquare, PipelineLifetime,
RTPassPushData, SwapchainTransferDst, UploadInFlight, VulkanBufferAddress,
VulkanShaderLifetime, WindowUpdateClock; others extended.
Breaking: SaveFrame is CaptureNextFrame; Mesh::opaque is geometryFlags; a
buffer without SHADER_DEVICE_ADDRESS usage has no address; frameCounter is
std::atomic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:54 +02:00
|
|
|
if (!CrafterWgiStart()) return;
|
|
|
|
|
|
|
|
|
|
// The connects and disconnects since the last tick.
|
|
|
|
|
std::array<CrafterWgiEvent, 16> events;
|
|
|
|
|
for (std::size_t count; (count = CrafterWgiTakeEvents(events.data(), events.size())) != 0;) {
|
|
|
|
|
for (const CrafterWgiEvent& e : std::span(events.data(), count)) {
|
|
|
|
|
if (e.added) {
|
|
|
|
|
if (g_byGamepad.find(e.gamepad) != g_byGamepad.end()) {
|
|
|
|
|
CrafterWgiRelease(e.gamepad);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
auto dev = std::make_unique<Crafter::Gamepad::Device>();
|
|
|
|
|
dev->id = PointerToId(e.gamepad);
|
|
|
|
|
dev->name = "Gamepad";
|
|
|
|
|
auto* raw = dev.get();
|
|
|
|
|
g_byGamepad[e.gamepad] = raw;
|
|
|
|
|
connected.push_back(std::move(dev));
|
|
|
|
|
onConnected.Invoke(raw);
|
|
|
|
|
// The event's reference is kept until the disconnect.
|
|
|
|
|
} else {
|
|
|
|
|
auto it = g_byGamepad.find(e.gamepad);
|
|
|
|
|
if (it != g_byGamepad.end()) {
|
|
|
|
|
Crafter::Gamepad::Device* raw = it->second;
|
|
|
|
|
onDisconnected.Invoke(raw);
|
|
|
|
|
for (auto vi = connected.begin(); vi != connected.end(); ++vi) {
|
|
|
|
|
if (vi->get() == raw) { connected.erase(vi); break; }
|
|
|
|
|
}
|
|
|
|
|
// The connect's reference, then the disconnect's.
|
|
|
|
|
CrafterWgiRelease(it->first);
|
|
|
|
|
g_byGamepad.erase(it);
|
2026-05-12 00:24:48 +02:00
|
|
|
}
|
feat: windows on their own clock, deferred deletion for every handle owner, staged re-uploads, and RTPass push data
Window
- updateInterval runs Update on the window's own clock while the
compositor sends no frame callbacks (hidden, minimized): the Wayland loop
is a poll() loop, and the Win32 loop sleeps until the next update. Both
read the interval each pass.
- SetPresentMode (FIFO, Mailbox, Immediate). A mode the surface lacks, or
whose swapchain has other than numFrames images (Mesa's Mailbox has four),
falls back to FIFO until the mode is set again.
- AddPollFd / RemovePollFd wake the Wayland loop on the app's descriptors.
- The swapchain has TRANSFER_DST where the surface allows it.
- CaptureNextFrame replaces SaveFrame: it records the copy in the frame,
before the present, where SaveFrame read an image already presented. A
frame unwound past before the write still frees the readback buffer.
- Inter-pass barriers on the swapchain image carry the access masks of the
stages the passes use.
Deferred deletion (issue #101)
- VulkanBuffer, Mesh, VulkanShader, ComputeShader and PipelineRTVulkan hand
their handles to the deletion queue from their destructors and moves, as
a frame in flight may still use them; a static owner goes after the
driver has torn down. VulkanShader and PipelineRTVulkan move and can't be
copied; re-Load and re-Init queue the old pipeline.
- The queue and its mutex are never destroyed, and the queue takes handles
from any thread; Device::frameCounter is atomic. Both the queue and the
staging rings count frames as one window renders them (documented).
- DrainDeletions also lets every buffer's staging ring start over: without
a Window nothing else reclaims either, so an app without one calls it
after waiting for its work.
Meshes and buffers
- A Mesh rebuild queues the old acceleration structure and builds into the
same storage after a barrier, so the BLAS address stays put when it fits.
The refit and rebuild barriers cover compute ray queries too.
- RTBuildOptions::noDuplicateAnyHit. Mesh::geometryFlags replaces opaque,
and the refit and rebuild fallbacks keep the flags.
- A re-upload into a buffer a frame in flight may read (UploadDeviceLocal
of the same size, UploadDeviceLocalRange) copies on the GPU, after those
reads, through a per-frame staging ring; on ReBAR it used to write the
buffer in place. Empty uploads record nothing.
- A compressed Mesh::Build that keeps its buffers decompresses into them
after the reads of frames in flight, and the hit shaders and ray queries
wait for the decompress as well as the BLAS build. Its vertex buffer
takes a ranged refit's copies.
- Mesh move-assigns (queueing the BLAS it replaces), so it can be replaced
in place or erased from a vector.
- VulkanBuffer::Create asks for a device address only for buffers with
SHADER_DEVICE_ADDRESS usage, as vkGetBufferDeviceAddress requires; the
others have address 0. UIRenderer::RegisterBuffer refuses those.
- Build inputs are synchronized as shader reads.
Other
- RTPass pushes the app's own push data and traces at an explicit size.
- DescriptorHeapVulkan hands freed slot ranges out again (they leaked), and
keeps its free lists sorted.
- ImageVulkan's mip chain clamps each level to 1 texel: a non-square
image's shorter side went to 0.
- VulkanShader throws on a missing file instead of failing in bad_alloc.
- CRAFTER_GRAPHICS_DEVICE picks the GPU by its number among the distinct
GPUs Vulkan lists; anything but such a number is an error.
- The debug messenger prints info messages only with
CRAFTER_GRAPHICS_VALIDATION set.
- Windows: the WinRT gamepad code moved to a C file (mingw-w64's WinRT
headers don't compile as C++), -lcombase dropped, a reference leak fixed.
- project.cpp honours --local for Crafter.Event, Math and Asset, as Asset
does for Math, and builds every test through one helper, projectArgs
included.
- Tests: DebugMessengerSeverity, DeletionQueueThreads, DeviceSelection,
HeapSlotReuse, MeshLifetime, MipChainNonSquare, PipelineLifetime,
RTPassPushData, SwapchainTransferDst, UploadInFlight, VulkanBufferAddress,
VulkanShaderLifetime, WindowUpdateClock; others extended.
Breaking: SaveFrame is CaptureNextFrame; Mesh::opaque is geometryFlags; a
buffer without SHADER_DEVICE_ADDRESS usage has no address; frameCounter is
std::atomic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:54 +02:00
|
|
|
CrafterWgiRelease(e.gamepad);
|
2026-05-12 00:24:48 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Poll each connected gamepad.
|
|
|
|
|
for (auto& [gp, dev] : g_byGamepad) {
|
feat: windows on their own clock, deferred deletion for every handle owner, staged re-uploads, and RTPass push data
Window
- updateInterval runs Update on the window's own clock while the
compositor sends no frame callbacks (hidden, minimized): the Wayland loop
is a poll() loop, and the Win32 loop sleeps until the next update. Both
read the interval each pass.
- SetPresentMode (FIFO, Mailbox, Immediate). A mode the surface lacks, or
whose swapchain has other than numFrames images (Mesa's Mailbox has four),
falls back to FIFO until the mode is set again.
- AddPollFd / RemovePollFd wake the Wayland loop on the app's descriptors.
- The swapchain has TRANSFER_DST where the surface allows it.
- CaptureNextFrame replaces SaveFrame: it records the copy in the frame,
before the present, where SaveFrame read an image already presented. A
frame unwound past before the write still frees the readback buffer.
- Inter-pass barriers on the swapchain image carry the access masks of the
stages the passes use.
Deferred deletion (issue #101)
- VulkanBuffer, Mesh, VulkanShader, ComputeShader and PipelineRTVulkan hand
their handles to the deletion queue from their destructors and moves, as
a frame in flight may still use them; a static owner goes after the
driver has torn down. VulkanShader and PipelineRTVulkan move and can't be
copied; re-Load and re-Init queue the old pipeline.
- The queue and its mutex are never destroyed, and the queue takes handles
from any thread; Device::frameCounter is atomic. Both the queue and the
staging rings count frames as one window renders them (documented).
- DrainDeletions also lets every buffer's staging ring start over: without
a Window nothing else reclaims either, so an app without one calls it
after waiting for its work.
Meshes and buffers
- A Mesh rebuild queues the old acceleration structure and builds into the
same storage after a barrier, so the BLAS address stays put when it fits.
The refit and rebuild barriers cover compute ray queries too.
- RTBuildOptions::noDuplicateAnyHit. Mesh::geometryFlags replaces opaque,
and the refit and rebuild fallbacks keep the flags.
- A re-upload into a buffer a frame in flight may read (UploadDeviceLocal
of the same size, UploadDeviceLocalRange) copies on the GPU, after those
reads, through a per-frame staging ring; on ReBAR it used to write the
buffer in place. Empty uploads record nothing.
- A compressed Mesh::Build that keeps its buffers decompresses into them
after the reads of frames in flight, and the hit shaders and ray queries
wait for the decompress as well as the BLAS build. Its vertex buffer
takes a ranged refit's copies.
- Mesh move-assigns (queueing the BLAS it replaces), so it can be replaced
in place or erased from a vector.
- VulkanBuffer::Create asks for a device address only for buffers with
SHADER_DEVICE_ADDRESS usage, as vkGetBufferDeviceAddress requires; the
others have address 0. UIRenderer::RegisterBuffer refuses those.
- Build inputs are synchronized as shader reads.
Other
- RTPass pushes the app's own push data and traces at an explicit size.
- DescriptorHeapVulkan hands freed slot ranges out again (they leaked), and
keeps its free lists sorted.
- ImageVulkan's mip chain clamps each level to 1 texel: a non-square
image's shorter side went to 0.
- VulkanShader throws on a missing file instead of failing in bad_alloc.
- CRAFTER_GRAPHICS_DEVICE picks the GPU by its number among the distinct
GPUs Vulkan lists; anything but such a number is an error.
- The debug messenger prints info messages only with
CRAFTER_GRAPHICS_VALIDATION set.
- Windows: the WinRT gamepad code moved to a C file (mingw-w64's WinRT
headers don't compile as C++), -lcombase dropped, a reference leak fixed.
- project.cpp honours --local for Crafter.Event, Math and Asset, as Asset
does for Math, and builds every test through one helper, projectArgs
included.
- Tests: DebugMessengerSeverity, DeletionQueueThreads, DeviceSelection,
HeapSlotReuse, MeshLifetime, MipChainNonSquare, PipelineLifetime,
RTPassPushData, SwapchainTransferDst, UploadInFlight, VulkanBufferAddress,
VulkanShaderLifetime, WindowUpdateClock; others extended.
Breaking: SaveFrame is CaptureNextFrame; Mesh::opaque is geometryFlags; a
buffer without SHADER_DEVICE_ADDRESS usage has no address; frameCounter is
std::atomic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:54 +02:00
|
|
|
CrafterWgiReading reading {};
|
|
|
|
|
if (!CrafterWgiRead(gp, &reading)) continue;
|
2026-05-12 00:24:48 +02:00
|
|
|
ApplyReading(dev, reading);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Crafter::Gamepad::Rumble(Device& dev, float low, float high,
|
|
|
|
|
std::chrono::milliseconds /*duration*/)
|
|
|
|
|
{
|
|
|
|
|
// WGI vibration is "set and forget" — no duration. Caller can stop
|
|
|
|
|
// by re-calling with zero magnitudes. We honor 0 duration as stop.
|
feat: windows on their own clock, deferred deletion for every handle owner, staged re-uploads, and RTPass push data
Window
- updateInterval runs Update on the window's own clock while the
compositor sends no frame callbacks (hidden, minimized): the Wayland loop
is a poll() loop, and the Win32 loop sleeps until the next update. Both
read the interval each pass.
- SetPresentMode (FIFO, Mailbox, Immediate). A mode the surface lacks, or
whose swapchain has other than numFrames images (Mesa's Mailbox has four),
falls back to FIFO until the mode is set again.
- AddPollFd / RemovePollFd wake the Wayland loop on the app's descriptors.
- The swapchain has TRANSFER_DST where the surface allows it.
- CaptureNextFrame replaces SaveFrame: it records the copy in the frame,
before the present, where SaveFrame read an image already presented. A
frame unwound past before the write still frees the readback buffer.
- Inter-pass barriers on the swapchain image carry the access masks of the
stages the passes use.
Deferred deletion (issue #101)
- VulkanBuffer, Mesh, VulkanShader, ComputeShader and PipelineRTVulkan hand
their handles to the deletion queue from their destructors and moves, as
a frame in flight may still use them; a static owner goes after the
driver has torn down. VulkanShader and PipelineRTVulkan move and can't be
copied; re-Load and re-Init queue the old pipeline.
- The queue and its mutex are never destroyed, and the queue takes handles
from any thread; Device::frameCounter is atomic. Both the queue and the
staging rings count frames as one window renders them (documented).
- DrainDeletions also lets every buffer's staging ring start over: without
a Window nothing else reclaims either, so an app without one calls it
after waiting for its work.
Meshes and buffers
- A Mesh rebuild queues the old acceleration structure and builds into the
same storage after a barrier, so the BLAS address stays put when it fits.
The refit and rebuild barriers cover compute ray queries too.
- RTBuildOptions::noDuplicateAnyHit. Mesh::geometryFlags replaces opaque,
and the refit and rebuild fallbacks keep the flags.
- A re-upload into a buffer a frame in flight may read (UploadDeviceLocal
of the same size, UploadDeviceLocalRange) copies on the GPU, after those
reads, through a per-frame staging ring; on ReBAR it used to write the
buffer in place. Empty uploads record nothing.
- A compressed Mesh::Build that keeps its buffers decompresses into them
after the reads of frames in flight, and the hit shaders and ray queries
wait for the decompress as well as the BLAS build. Its vertex buffer
takes a ranged refit's copies.
- Mesh move-assigns (queueing the BLAS it replaces), so it can be replaced
in place or erased from a vector.
- VulkanBuffer::Create asks for a device address only for buffers with
SHADER_DEVICE_ADDRESS usage, as vkGetBufferDeviceAddress requires; the
others have address 0. UIRenderer::RegisterBuffer refuses those.
- Build inputs are synchronized as shader reads.
Other
- RTPass pushes the app's own push data and traces at an explicit size.
- DescriptorHeapVulkan hands freed slot ranges out again (they leaked), and
keeps its free lists sorted.
- ImageVulkan's mip chain clamps each level to 1 texel: a non-square
image's shorter side went to 0.
- VulkanShader throws on a missing file instead of failing in bad_alloc.
- CRAFTER_GRAPHICS_DEVICE picks the GPU by its number among the distinct
GPUs Vulkan lists; anything but such a number is an error.
- The debug messenger prints info messages only with
CRAFTER_GRAPHICS_VALIDATION set.
- Windows: the WinRT gamepad code moved to a C file (mingw-w64's WinRT
headers don't compile as C++), -lcombase dropped, a reference leak fixed.
- project.cpp honours --local for Crafter.Event, Math and Asset, as Asset
does for Math, and builds every test through one helper, projectArgs
included.
- Tests: DebugMessengerSeverity, DeletionQueueThreads, DeviceSelection,
HeapSlotReuse, MeshLifetime, MipChainNonSquare, PipelineLifetime,
RTPassPushData, SwapchainTransferDst, UploadInFlight, VulkanBufferAddress,
VulkanShaderLifetime, WindowUpdateClock; others extended.
Breaking: SaveFrame is CaptureNextFrame; Mesh::opaque is geometryFlags; a
buffer without SHADER_DEVICE_ADDRESS usage has no address; frameCounter is
std::atomic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:54 +02:00
|
|
|
CrafterWgiGamepad* gp = nullptr;
|
2026-05-12 00:24:48 +02:00
|
|
|
for (auto& [g, d] : g_byGamepad) {
|
|
|
|
|
if (d == &dev) { gp = g; break; }
|
|
|
|
|
}
|
|
|
|
|
if (!gp) return;
|
|
|
|
|
auto clamp01 = [](float v) { return v < 0 ? 0.0f : v > 1.0f ? 1.0f : v; };
|
feat: windows on their own clock, deferred deletion for every handle owner, staged re-uploads, and RTPass push data
Window
- updateInterval runs Update on the window's own clock while the
compositor sends no frame callbacks (hidden, minimized): the Wayland loop
is a poll() loop, and the Win32 loop sleeps until the next update. Both
read the interval each pass.
- SetPresentMode (FIFO, Mailbox, Immediate). A mode the surface lacks, or
whose swapchain has other than numFrames images (Mesa's Mailbox has four),
falls back to FIFO until the mode is set again.
- AddPollFd / RemovePollFd wake the Wayland loop on the app's descriptors.
- The swapchain has TRANSFER_DST where the surface allows it.
- CaptureNextFrame replaces SaveFrame: it records the copy in the frame,
before the present, where SaveFrame read an image already presented. A
frame unwound past before the write still frees the readback buffer.
- Inter-pass barriers on the swapchain image carry the access masks of the
stages the passes use.
Deferred deletion (issue #101)
- VulkanBuffer, Mesh, VulkanShader, ComputeShader and PipelineRTVulkan hand
their handles to the deletion queue from their destructors and moves, as
a frame in flight may still use them; a static owner goes after the
driver has torn down. VulkanShader and PipelineRTVulkan move and can't be
copied; re-Load and re-Init queue the old pipeline.
- The queue and its mutex are never destroyed, and the queue takes handles
from any thread; Device::frameCounter is atomic. Both the queue and the
staging rings count frames as one window renders them (documented).
- DrainDeletions also lets every buffer's staging ring start over: without
a Window nothing else reclaims either, so an app without one calls it
after waiting for its work.
Meshes and buffers
- A Mesh rebuild queues the old acceleration structure and builds into the
same storage after a barrier, so the BLAS address stays put when it fits.
The refit and rebuild barriers cover compute ray queries too.
- RTBuildOptions::noDuplicateAnyHit. Mesh::geometryFlags replaces opaque,
and the refit and rebuild fallbacks keep the flags.
- A re-upload into a buffer a frame in flight may read (UploadDeviceLocal
of the same size, UploadDeviceLocalRange) copies on the GPU, after those
reads, through a per-frame staging ring; on ReBAR it used to write the
buffer in place. Empty uploads record nothing.
- A compressed Mesh::Build that keeps its buffers decompresses into them
after the reads of frames in flight, and the hit shaders and ray queries
wait for the decompress as well as the BLAS build. Its vertex buffer
takes a ranged refit's copies.
- Mesh move-assigns (queueing the BLAS it replaces), so it can be replaced
in place or erased from a vector.
- VulkanBuffer::Create asks for a device address only for buffers with
SHADER_DEVICE_ADDRESS usage, as vkGetBufferDeviceAddress requires; the
others have address 0. UIRenderer::RegisterBuffer refuses those.
- Build inputs are synchronized as shader reads.
Other
- RTPass pushes the app's own push data and traces at an explicit size.
- DescriptorHeapVulkan hands freed slot ranges out again (they leaked), and
keeps its free lists sorted.
- ImageVulkan's mip chain clamps each level to 1 texel: a non-square
image's shorter side went to 0.
- VulkanShader throws on a missing file instead of failing in bad_alloc.
- CRAFTER_GRAPHICS_DEVICE picks the GPU by its number among the distinct
GPUs Vulkan lists; anything but such a number is an error.
- The debug messenger prints info messages only with
CRAFTER_GRAPHICS_VALIDATION set.
- Windows: the WinRT gamepad code moved to a C file (mingw-w64's WinRT
headers don't compile as C++), -lcombase dropped, a reference leak fixed.
- project.cpp honours --local for Crafter.Event, Math and Asset, as Asset
does for Math, and builds every test through one helper, projectArgs
included.
- Tests: DebugMessengerSeverity, DeletionQueueThreads, DeviceSelection,
HeapSlotReuse, MeshLifetime, MipChainNonSquare, PipelineLifetime,
RTPassPushData, SwapchainTransferDst, UploadInFlight, VulkanBufferAddress,
VulkanShaderLifetime, WindowUpdateClock; others extended.
Breaking: SaveFrame is CaptureNextFrame; Mesh::opaque is geometryFlags; a
buffer without SHADER_DEVICE_ADDRESS usage has no address; frameCounter is
std::atomic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:54 +02:00
|
|
|
CrafterWgiVibrate(gp, clamp01(low), clamp01(high));
|
2026-05-12 00:24:48 +02:00
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
2026-05-18 02:07:48 +02:00
|
|
|
// ────────────────────────────────────────────────────────────────────
|
|
|
|
|
// DOM backend (browser Gamepad API via env.js polling helpers)
|
|
|
|
|
// ────────────────────────────────────────────────────────────────────
|
|
|
|
|
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
|
|
|
|
|
namespace Crafter::DomEnv {
|
|
|
|
|
// External linkage required for the import_module attribute to bind
|
|
|
|
|
// against the env.js function. An anonymous namespace would drop the
|
|
|
|
|
// attribute at link time.
|
|
|
|
|
__attribute__((import_module("env"), import_name("gamepadPollConnected")))
|
|
|
|
|
bool gamepadPollConnected();
|
|
|
|
|
__attribute__((import_module("env"), import_name("gamepadPollDisconnected")))
|
|
|
|
|
bool gamepadPollDisconnected();
|
|
|
|
|
__attribute__((import_module("env"), import_name("gamepadCount")))
|
|
|
|
|
std::int32_t gamepadCount();
|
|
|
|
|
__attribute__((import_module("env"), import_name("gamepadGetButton")))
|
|
|
|
|
std::int32_t gamepadGetButton(std::int32_t idx, std::int32_t buttonIdx);
|
|
|
|
|
__attribute__((import_module("env"), import_name("gamepadGetAxis")))
|
|
|
|
|
double gamepadGetAxis(std::int32_t idx, std::int32_t axisIdx);
|
|
|
|
|
}
|
|
|
|
|
namespace {
|
|
|
|
|
using Crafter::DomEnv::gamepadPollConnected;
|
|
|
|
|
using Crafter::DomEnv::gamepadPollDisconnected;
|
|
|
|
|
using Crafter::DomEnv::gamepadCount;
|
|
|
|
|
using Crafter::DomEnv::gamepadGetButton;
|
|
|
|
|
using Crafter::DomEnv::gamepadGetAxis;
|
|
|
|
|
|
|
|
|
|
// Standard W3C Gamepad mapping: indices match the "standard" layout
|
|
|
|
|
// every browser exposes for Xbox / DualShock / DualSense controllers.
|
|
|
|
|
// Mismatched / non-standard pads fall through with zeroed buttons.
|
|
|
|
|
constexpr int kStdBtnSouth = 0;
|
|
|
|
|
constexpr int kStdBtnEast = 1;
|
|
|
|
|
constexpr int kStdBtnWest = 2;
|
|
|
|
|
constexpr int kStdBtnNorth = 3;
|
|
|
|
|
constexpr int kStdBtnLB = 4;
|
|
|
|
|
constexpr int kStdBtnRB = 5;
|
|
|
|
|
constexpr int kStdBtnLT = 6;
|
|
|
|
|
constexpr int kStdBtnRT = 7;
|
|
|
|
|
constexpr int kStdBtnSelect = 8;
|
|
|
|
|
constexpr int kStdBtnStart = 9;
|
|
|
|
|
constexpr int kStdBtnLStickClick = 10;
|
|
|
|
|
constexpr int kStdBtnRStickClick = 11;
|
|
|
|
|
constexpr int kStdBtnDPadUp = 12;
|
|
|
|
|
constexpr int kStdBtnDPadDown = 13;
|
|
|
|
|
constexpr int kStdBtnDPadLeft = 14;
|
|
|
|
|
constexpr int kStdBtnDPadRight = 15;
|
|
|
|
|
constexpr int kStdBtnHome = 16;
|
|
|
|
|
|
|
|
|
|
constexpr int kStdAxisLX = 0, kStdAxisLY = 1, kStdAxisRX = 2, kStdAxisRY = 3;
|
|
|
|
|
|
|
|
|
|
// Map our `Gamepad::Button` ordinal to the W3C standard button index.
|
|
|
|
|
int StdButtonIdx(Crafter::Gamepad::Button b) {
|
|
|
|
|
using B = Crafter::Gamepad::Button;
|
|
|
|
|
switch (b) {
|
|
|
|
|
case B::South: return kStdBtnSouth;
|
|
|
|
|
case B::East: return kStdBtnEast;
|
|
|
|
|
case B::West: return kStdBtnWest;
|
|
|
|
|
case B::North: return kStdBtnNorth;
|
|
|
|
|
case B::Select: return kStdBtnSelect;
|
|
|
|
|
case B::Start: return kStdBtnStart;
|
|
|
|
|
case B::Home: return kStdBtnHome;
|
|
|
|
|
case B::LeftStickClick: return kStdBtnLStickClick;
|
|
|
|
|
case B::RightStickClick: return kStdBtnRStickClick;
|
|
|
|
|
case B::LeftBumper: return kStdBtnLB;
|
|
|
|
|
case B::RightBumper: return kStdBtnRB;
|
|
|
|
|
case B::DPadUp: return kStdBtnDPadUp;
|
|
|
|
|
case B::DPadDown: return kStdBtnDPadDown;
|
|
|
|
|
case B::DPadLeft: return kStdBtnDPadLeft;
|
|
|
|
|
case B::DPadRight: return kStdBtnDPadRight;
|
|
|
|
|
case B::LeftTrigger: return kStdBtnLT;
|
|
|
|
|
case B::RightTrigger: return kStdBtnRT;
|
|
|
|
|
default: return -1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Crafter::Gamepad::Tick() {
|
|
|
|
|
// Handle connect/disconnect by rebuilding `connected` from scratch
|
|
|
|
|
// when the JS flags say something changed. Cheap — only fires on
|
|
|
|
|
// actual hot-plug, not every frame.
|
|
|
|
|
if (gamepadPollConnected() || gamepadPollDisconnected() || connected.empty()) {
|
|
|
|
|
// Snapshot previously-known ids so we can fire onDisconnected
|
|
|
|
|
// for ones that went away in this rebuild.
|
|
|
|
|
std::vector<std::uint32_t> previousIds;
|
|
|
|
|
previousIds.reserve(connected.size());
|
|
|
|
|
for (auto& d : connected) previousIds.push_back(d->id);
|
|
|
|
|
|
|
|
|
|
std::int32_t n = gamepadCount();
|
|
|
|
|
connected.clear();
|
|
|
|
|
for (std::int32_t i = 0; i < n; ++i) {
|
|
|
|
|
auto dev = std::make_unique<Device>();
|
|
|
|
|
dev->id = static_cast<std::uint32_t>(i); // index-based; stable for the page
|
|
|
|
|
dev->name = "Gamepad";
|
|
|
|
|
connected.push_back(std::move(dev));
|
|
|
|
|
}
|
|
|
|
|
// Fire connected events for ids that weren't in the previous set.
|
|
|
|
|
for (auto& d : connected) {
|
|
|
|
|
bool wasKnown = false;
|
|
|
|
|
for (std::uint32_t prev : previousIds) if (prev == d->id) { wasKnown = true; break; }
|
|
|
|
|
if (!wasKnown) onConnected.Invoke(d.get());
|
|
|
|
|
}
|
|
|
|
|
// No way to call onDisconnected with the right Device* once it's
|
|
|
|
|
// gone — fire with nullptr so subscribers know SOMETHING changed.
|
|
|
|
|
for (std::uint32_t prev : previousIds) {
|
|
|
|
|
bool stillThere = false;
|
|
|
|
|
for (auto& d : connected) if (d->id == prev) { stillThere = true; break; }
|
|
|
|
|
if (!stillThere) onDisconnected.Invoke(nullptr);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Poll buttons + axes on every Tick — same shape as the native paths.
|
|
|
|
|
for (auto& devUp : connected) {
|
|
|
|
|
Device& dev = *devUp;
|
|
|
|
|
std::int32_t idx = static_cast<std::int32_t>(dev.id);
|
|
|
|
|
|
|
|
|
|
for (std::size_t b = 0; b < (std::size_t)Button::Max; ++b) {
|
|
|
|
|
int stdIdx = StdButtonIdx(static_cast<Button>(b));
|
|
|
|
|
bool newState = stdIdx >= 0 && gamepadGetButton(idx, stdIdx) != 0;
|
|
|
|
|
if (newState != dev.buttons[b]) {
|
|
|
|
|
dev.buttons[b] = newState;
|
|
|
|
|
if (newState) dev.onButtonDown.Invoke(static_cast<Button>(b));
|
|
|
|
|
else dev.onButtonUp .Invoke(static_cast<Button>(b));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Axes — sticks come straight through, triggers come from button
|
|
|
|
|
// analog values (the standard mapping exposes those too, with
|
|
|
|
|
// buttons[6/7].value, but the polling shim returns binary
|
|
|
|
|
// pressed-state. For V1, triggers report 0 / 1 only).
|
|
|
|
|
float lx = static_cast<float>(gamepadGetAxis(idx, kStdAxisLX));
|
|
|
|
|
float ly = static_cast<float>(gamepadGetAxis(idx, kStdAxisLY));
|
|
|
|
|
float rx = static_cast<float>(gamepadGetAxis(idx, kStdAxisRX));
|
|
|
|
|
float ry = static_cast<float>(gamepadGetAxis(idx, kStdAxisRY));
|
|
|
|
|
auto setAxis = [&](Axis a, float v) {
|
|
|
|
|
std::size_t i = (std::size_t)a;
|
|
|
|
|
if (dev.axes[i] != v) {
|
|
|
|
|
dev.axes[i] = v;
|
|
|
|
|
dev.onAxisChanged.Invoke(a);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
setAxis(Axis::LeftStickX, lx);
|
|
|
|
|
setAxis(Axis::LeftStickY, ly);
|
|
|
|
|
setAxis(Axis::RightStickX, rx);
|
|
|
|
|
setAxis(Axis::RightStickY, ry);
|
|
|
|
|
setAxis(Axis::LeftTrigger, gamepadGetButton(idx, kStdBtnLT) ? 1.0f : 0.0f);
|
|
|
|
|
setAxis(Axis::RightTrigger, gamepadGetButton(idx, kStdBtnRT) ? 1.0f : 0.0f);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Crafter::Gamepad::Rumble(Device& /*dev*/, float /*low*/, float /*high*/,
|
|
|
|
|
std::chrono::milliseconds /*duration*/) {
|
|
|
|
|
// Browser Gamepad rumble (HapticActuator API) is patchy across
|
|
|
|
|
// engines. V1: no-op. Pads that don't support it would silently
|
|
|
|
|
// fall through anyway.
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
2026-05-12 00:24:48 +02:00
|
|
|
Crafter::Gamepad::Device* Crafter::Gamepad::FindById(std::uint32_t id) {
|
|
|
|
|
for (auto& up : connected) {
|
|
|
|
|
if (up->id == id) return up.get();
|
|
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|