new input system

This commit is contained in:
Jorijn van der Graaf 2026-05-12 00:24:48 +02:00
commit ac2eb7fb0a
31 changed files with 3292 additions and 781 deletions

View file

@ -0,0 +1,42 @@
/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
Catcrafts.net
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3.0 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
export module Crafter.Graphics:Clipboard;
import std;
// Native system-clipboard writes. No popen, no helper binaries — just
// the platform's own clipboard API. Implementation lives next to the
// other window-backend code (Wayland data_device on Linux, Win32 in
// the Windows build); callers don't pick a backend.
//
// `Get` is intentionally not exposed yet: paste-from-clipboard isn't
// a feature the game's UI wants right now, and the read path needs
// more lifecycle plumbing (mime negotiation, fd reads on the Wayland
// event loop) than the simple write path. Easy to add later.
export namespace Crafter::Clipboard {
// Place `text` on the system clipboard as UTF-8 plain text. Returns
// true if the platform accepted the request — false means the
// backend isn't initialised, no input event has been seen yet
// (Wayland needs a recent serial), or the OS API failed. On
// success the ownership of the clipboard contents is held until
// either another app replaces the selection or the application
// exits; the caller doesn't need to keep `text` alive.
bool SetText(std::string_view text);
}

View file

@ -0,0 +1,133 @@
/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
catcrafts.net
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License version 3.0 as published by the Free Software Foundation;
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
module;
#include "vulkan/vulkan.h"
export module Crafter.Graphics:Decompress;
import :Device;
import std;
export namespace Crafter::Decompress {
// GDeflate 1.0 requires each VkDecompressMemoryRegionEXT to cover one
// 64 KiB tile. CompressedBlob stores one stream per RegionMeta — this
// helper walks the stream's tile-stream header and emits per-tile
// regions ready for vkCmdDecompressMemoryEXT.
//
// streamBytes is the raw stream bytes (CompressedBlob::bytes.subspan(
// r.srcOffset, r.compressedSize)). srcBase is its GPU device address.
// dstBase is the GPU device address of the first decompressed byte.
inline void ExpandStreamToTileRegions(
std::span<const std::byte> streamBytes,
VkDeviceAddress srcBase,
VkDeviceAddress dstBase,
std::vector<VkDecompressMemoryRegionEXT>& out)
{
if (streamBytes.empty()) return;
constexpr std::size_t kTileSize = 64 * 1024;
constexpr std::size_t kHeaderSize = 8;
// TileStream wire layout (see vendored TileStream.h):
// u8 id; u8 magic; u16 numTiles;
// { u2 tileSizeIdx; u18 lastTileSize; u12 reserved; } packed into u32
const std::uint8_t* p = reinterpret_cast<const std::uint8_t*>(streamBytes.data());
std::uint8_t id = p[0];
std::uint8_t magic = p[1];
if (id != (magic ^ 0xff)) {
throw std::runtime_error("GDeflate tile-stream: bad id/magic");
}
std::uint16_t numTiles =
static_cast<std::uint16_t>(p[2])
| static_cast<std::uint16_t>(p[3]) << 8;
std::uint32_t packed =
static_cast<std::uint32_t>(p[4])
| static_cast<std::uint32_t>(p[5]) << 8
| static_cast<std::uint32_t>(p[6]) << 16
| static_cast<std::uint32_t>(p[7]) << 24;
std::uint32_t lastTileSize = (packed >> 2) & 0x3FFFFu;
const std::uint32_t* tileOffsets = reinterpret_cast<const std::uint32_t*>(
streamBytes.data() + kHeaderSize);
const std::size_t dataStart = kHeaderSize + std::size_t(numTiles) * sizeof(std::uint32_t);
out.reserve(out.size() + numTiles);
for (std::uint32_t k = 0; k < numTiles; ++k) {
std::size_t tileOff = (k > 0) ? tileOffsets[k] : 0;
std::size_t compressedSize = (k + 1 < numTiles)
? std::size_t(tileOffsets[k + 1]) - tileOff
: std::size_t(tileOffsets[0]);
std::size_t decompressedSize = (k + 1 < numTiles)
? kTileSize
: (lastTileSize != 0 ? std::size_t(lastTileSize) : kTileSize);
out.push_back(VkDecompressMemoryRegionEXT {
.srcAddress = srcBase + dataStart + tileOff,
.dstAddress = dstBase + std::size_t(k) * kTileSize,
.compressedSize = compressedSize,
.decompressedSize = decompressedSize,
});
}
}
// Records vkCmdDecompressMemoryEXT into `cmd` for the supplied regions
// (caller pre-resolves srcAddress/dstAddress) and follows it with a
// synchronization2 memory barrier so the consumer at (dstStage, dstAccess)
// observes the decompressed bytes.
//
// Method is fixed at GDeflate 1.0 — Crafter.Asset's compressed file format
// emits GDeflate streams. Caller must guarantee Device::memoryDecompressionSupported.
inline void DecompressOnGPU(
VkCommandBuffer cmd,
std::span<const VkDecompressMemoryRegionEXT> regions,
VkPipelineStageFlags2 dstStage,
VkAccessFlags2 dstAccess)
{
if (regions.empty()) return;
VkDecompressMemoryInfoEXT info {
.sType = VK_STRUCTURE_TYPE_DECOMPRESS_MEMORY_INFO_EXT,
.pNext = nullptr,
.decompressionMethod = VK_MEMORY_DECOMPRESSION_METHOD_GDEFLATE_1_0_BIT_EXT,
.regionCount = static_cast<std::uint32_t>(regions.size()),
.pRegions = regions.data(),
};
Device::vkCmdDecompressMemoryEXT(cmd, &info);
VkMemoryBarrier2 barrier {
.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2,
.pNext = nullptr,
.srcStageMask = VK_PIPELINE_STAGE_2_MEMORY_DECOMPRESSION_BIT_EXT,
.srcAccessMask = VK_ACCESS_2_MEMORY_DECOMPRESSION_WRITE_BIT_EXT,
.dstStageMask = dstStage,
.dstAccessMask = dstAccess,
};
VkDependencyInfo dep {
.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,
.pNext = nullptr,
.dependencyFlags = 0,
.memoryBarrierCount = 1,
.pMemoryBarriers = &barrier,
.bufferMemoryBarrierCount = 0,
.pBufferMemoryBarriers = nullptr,
.imageMemoryBarrierCount = 0,
.pImageMemoryBarriers = nullptr,
};
vkCmdPipelineBarrier2(cmd, &dep);
}
}

View file

@ -30,7 +30,7 @@ module;
#endif
export module Crafter.Graphics:Device;
import std;
import :Types; // CrafterKeys for keyboard repeat state
import :Keys; // KeyCode for keyboard repeat state
export namespace Crafter {
struct Window;
@ -43,7 +43,7 @@ export namespace Crafter {
int rate = 25; // chars/sec
int delay = 500; // ms before first repeat
bool active = false;
CrafterKeys key{};
KeyCode key = 0;
std::string utf8; // UTF-8 to re-emit as onTextInput, if any
std::chrono::time_point<std::chrono::steady_clock> pressTime;
std::chrono::time_point<std::chrono::steady_clock> lastFireTime;
@ -68,6 +68,13 @@ export namespace Crafter {
inline static xkb_state* xkb_state;
inline static std::vector<Window*> windows;
inline static wl_pointer* wlPointer;
// wl_data_device_manager + wl_data_device drive copy/paste. Bound
// lazily in handle_global; the data device is created once both
// the manager and the seat are present (registry binding order
// isn't guaranteed). nullptr on compositors that don't expose
// the manager — Clipboard::SetText silently no-ops there.
inline static wl_data_device_manager* dataDeviceManager = nullptr;
inline static wl_data_device* dataDevice = nullptr;
static void seat_handle_capabilities(void* data, wl_seat* seat, uint32_t capabilities);
static void xdg_surface_handle_preferred_scale(void* data, wp_fractional_scale_v1*, std::uint32_t scale);
@ -137,6 +144,12 @@ export namespace Crafter {
inline static PFN_vkGetPhysicalDeviceDescriptorSizeEXT vkGetPhysicalDeviceDescriptorSizeEXT;
inline static PFN_vkGetDeviceFaultInfoEXT vkGetDeviceFaultInfoEXT;
// VK_EXT_memory_decompression — opt-in. When the driver advertises it
// and exposes the GDeflate 1.0 method, GPU asset decompression is
// available; otherwise consumers fall back to CPU decode.
inline static bool memoryDecompressionSupported = false;
inline static PFN_vkCmdDecompressMemoryEXT vkCmdDecompressMemoryEXT = nullptr;
inline static VkPhysicalDeviceMemoryProperties memoryProperties;
inline static VkPhysicalDeviceDescriptorHeapPropertiesEXT descriptorHeapProperties = {
@ -146,16 +159,19 @@ export namespace Crafter {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_TRACING_PIPELINE_PROPERTIES_KHR,
.pNext = &descriptorHeapProperties
};
inline static VkPhysicalDeviceMemoryDecompressionPropertiesEXT memoryDecompressionProperties = {
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_DECOMPRESSION_PROPERTIES_EXT
};
static void CheckVkResult(VkResult result);
static std::uint32_t GetMemoryType(std::uint32_t typeBits, VkMemoryPropertyFlags properties);
// ─── Wayland key repeat ────────────────────────────────────────
// TickKeyRepeats walks the held-key state and fires onKeyDown /
// onTextInput accordingly. Called once per frame from
// Window::Render. KeyRepeatState lives at namespace scope so its
// member initializers don't trip C++'s "complete-type-needed"
// rule for the inline static below.
// TickKeyRepeats fires onRawKeyDown / onRawKeyHold / onTextInput on
// the focused window for whichever key is currently repeating.
// Called once per frame from Window::Render. KeyRepeatState lives
// at namespace scope so its member initializers don't trip C++'s
// "complete-type-needed" rule for the inline static below.
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
inline static KeyRepeatState keyRepeat;
static void TickKeyRepeats();

View file

@ -0,0 +1,99 @@
/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
Catcrafts.net
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3.0 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
export module Crafter.Graphics:Gamepad;
import std;
import Crafter.Event;
// Raw gamepad device API. Platform-split implementation:
// - Linux: libudev (enumerate + hot-plug) + libevdev (read events,
// calibrate axes). Open every /dev/input/event* node the kernel
// classifies as a gamepad; ignore the rest.
// - Windows: Windows.Gaming.Input via the C ABI (COBJMACROS pattern,
// same as SDL). Added/Removed events fire on a thread pool and are
// marshaled into a queue drained from Tick() on the main thread.
//
// Button names follow the standardized kernel BTN_SOUTH / BTN_EAST /
// BTN_WEST / BTN_NORTH convention — physical position, not label. An
// Xbox "A" and a PlayStation "Cross" both arrive as `South`.
//
// Stick axes are -1..1 (post-deadzone calibration, raw float). Trigger
// axes are 0..1. The `LeftTrigger` / `RightTrigger` entries in `Button`
// and `Axis` are both populated — pick whichever fits your use case
// (digital threshold vs. analog reading).
export namespace Crafter::Gamepad {
enum class Button : std::uint8_t {
South, East, West, North, // BTN_SOUTH/EAST/WEST/NORTH
Select, Start, Home,
LeftStickClick, RightStickClick,
LeftBumper, RightBumper,
DPadUp, DPadDown, DPadLeft, DPadRight,
LeftTrigger, RightTrigger, // digital threshold; analog also on Axis
Max
};
enum class Axis : std::uint8_t {
LeftStickX, LeftStickY,
RightStickX, RightStickY,
LeftTrigger, RightTrigger,
Max
};
enum class Stick : std::uint8_t { Left, Right };
struct Device {
std::uint32_t id; // stable across the device's lifetime
std::string name;
bool buttons[(std::size_t)Button::Max] = {};
float axes[(std::size_t)Axis::Max] = {}; // sticks: -1..1, triggers: 0..1
Event<Button> onButtonDown;
Event<Button> onButtonUp;
Event<Axis> onAxisChanged;
};
// Process-level state — one input subsystem per app. Indices into
// `connected` are NOT stable across hot-plug; use `Device::id` when
// serializing bindings. The list is unsorted; iterate to find a
// specific id.
inline std::vector<std::unique_ptr<Device>> connected;
inline Event<Device*> onConnected;
inline Event<Device*> onDisconnected;
// Drains pending events from the OS, fires the relevant onButton* /
// onAxisChanged / onConnected / onDisconnected events synchronously,
// and updates the polled `buttons[]` / `axes[]` state on each Device.
// Must be called from the main thread once per frame — Window's
// event loop does this automatically. Safe to call when no gamepads
// are connected (early-out path).
void Tick();
// Optional rumble. `low` drives the heavy/low-frequency motor,
// `high` drives the light/high-frequency motor (Xbox naming). Both
// are clamped to 0..1. `duration` clamps to the backend's max; pass
// 0ms to stop any active rumble. No-op if the device doesn't
// advertise force-feedback.
void Rumble(Device& dev, float low, float high,
std::chrono::milliseconds duration);
// Find a connected device by its stable id. Returns nullptr if the
// device isn't currently connected — typical use after deserializing
// a saved binding.
Device* FindById(std::uint32_t id);
}

View file

@ -23,6 +23,8 @@ module;
export module Crafter.Graphics:ImageVulkan;
import std;
import Crafter.Asset;
import :Decompress;
import :VulkanBuffer;
export namespace Crafter {
@ -35,6 +37,10 @@ export namespace Crafter {
VkImage image;
VkDeviceMemory imageMemory;
VulkanBuffer<PixelType, true> buffer;
// Lives until the compressed Update path's cmd buffer completes.
// Same lifetime contract as Mesh::compressedStaging — caller must
// not destroy / re-Update before the submit fence is signaled.
VulkanBuffer<std::byte, true> compressedStaging;
VkImageView imageView;
VkDescriptorImageInfo descriptor;
@ -153,6 +159,114 @@ export namespace Crafter {
}
}
// GPU compressed-asset Update: stage compressed bytes, decompress
// into `buffer` via VK_EXT_memory_decompression, then copy buffer→image
// and transition to `layout`. Falls back to CPU decode + the existing
// Update path when Device::memoryDecompressionSupported is false.
// Caller is responsible for the dimensions matching: asset.sizeX/sizeY
// must equal this->width/height (set by Create), and asset.pixelStride
// must equal sizeof(PixelType).
void Update(const CompressedTextureAsset& asset, VkCommandBuffer cmd, VkImageLayout layout) {
if (asset.pixelStride != sizeof(PixelType)) {
throw std::runtime_error("ImageVulkan::Update(compressed): pixel stride mismatch");
}
if (!Device::memoryDecompressionSupported) {
std::span<PixelType> dst{ buffer.value, static_cast<std::size_t>(width) * height };
std::array<std::span<std::byte>, 1> outputs = {
std::as_writable_bytes(dst),
};
Compression::DecompressCPU(asset.blob, outputs);
Update(cmd, layout);
return;
}
// Re-create the staging-into-image buffer with MEMORY_DECOMPRESSION
// permission so the GPU codec can write into it. Keeps it
// HOST_VISIBLE (matches the existing path) — on UMA / ReBAR that's
// a fast path, on older systems the decompress writes traverse
// PCIe but correctness is unchanged.
buffer.Resize(
VK_BUFFER_USAGE_TRANSFER_SRC_BIT
| VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT
| VK_BUFFER_USAGE_2_MEMORY_DECOMPRESSION_BIT_EXT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
static_cast<std::uint32_t>(width) * height);
compressedStaging.Resize(
VK_BUFFER_USAGE_TRANSFER_SRC_BIT
| VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT
| VK_BUFFER_USAGE_2_MEMORY_DECOMPRESSION_BIT_EXT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
static_cast<std::uint32_t>(asset.blob.bytes.size()));
std::memcpy(compressedStaging.value, asset.blob.bytes.data(), asset.blob.bytes.size());
compressedStaging.FlushDevice();
std::vector<VkDecompressMemoryRegionEXT> regions;
for (const Compression::RegionMeta& r : asset.blob.regions) {
if (r.decompressedSize == 0) continue;
std::span<const std::byte> streamBytes(
asset.blob.bytes.data() + r.srcOffset,
static_cast<std::size_t>(r.compressedSize));
Decompress::ExpandStreamToTileRegions(
streamBytes,
compressedStaging.address + r.srcOffset,
buffer.address,
regions);
}
Decompress::DecompressOnGPU(
cmd,
regions,
VK_PIPELINE_STAGE_2_COPY_BIT,
VK_ACCESS_2_TRANSFER_READ_BIT);
// Continue with the existing buffer→image upload + layout transitions.
// We've already inserted the decompress→transfer-read barrier,
// so we skip the FlushDevice host-write barrier the regular Update
// would emit (no host write happened).
TransitionImageLayout(cmd, image, layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, 0, mipLevels);
VkBufferImageCopy region{};
region.bufferOffset = 0;
region.bufferRowLength = 0;
region.bufferImageHeight = 0;
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
region.imageSubresource.mipLevel = 0;
region.imageSubresource.baseArrayLayer = 0;
region.imageSubresource.layerCount = 1;
region.imageOffset = {0, 0, 0};
region.imageExtent = { width, height, 1 };
vkCmdCopyBufferToImage(cmd, buffer.buffer, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
if (mipLevels > 1) {
TransitionImageLayout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, 0, 1);
for (std::uint16_t i = 1; i < mipLevels; ++i) {
std::uint16_t mipWidth = width >> i;
std::uint16_t mipHeight = height >> i;
std::uint16_t previousMipWidth = width >> (i - std::uint16_t(1));
std::uint16_t previousMipHeight = height >> (i - std::uint16_t(1));
VkImageBlit blit = {};
blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
blit.srcSubresource.mipLevel = i - 1;
blit.srcSubresource.baseArrayLayer = 0;
blit.srcSubresource.layerCount = 1;
blit.srcOffsets[0] = { 0, 0, 0 };
blit.srcOffsets[1] = { (int32_t)previousMipWidth, (int32_t)previousMipHeight, 1 };
blit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
blit.dstSubresource.mipLevel = i;
blit.dstSubresource.baseArrayLayer = 0;
blit.dstSubresource.layerCount = 1;
blit.dstOffsets[0] = { 0, 0, 0 };
blit.dstOffsets[1] = { (int32_t)mipWidth, (int32_t)mipHeight, 1 };
vkCmdBlitImage(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR);
TransitionImageLayout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, i, 1);
}
TransitionImageLayout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, layout, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT, 0, mipLevels);
} else {
TransitionImageLayout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, layout, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, 0, mipLevels);
}
}
void Destroy() {
vkDestroyImageView(Device::device, imageView, nullptr);
vkDestroyImage(Device::device, image, nullptr);

View file

@ -0,0 +1,180 @@
/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
Catcrafts.net
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3.0 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
export module Crafter.Graphics:Input;
import std;
import Crafter.Math;
import Crafter.Event;
import :Keys;
import :Gamepad;
import :Window;
// Named-action input mapping with rebindable bindings. Game code declares
// actions ("Jump", "Move", "Fire"), attaches binding sources (keys, mouse
// buttons, gamepad sticks, WASD-as-Vector2, etc.), and subscribes to
// `onPerformed` / `onCanceled` / `onValueChanged` events. Bindings can be
// rebound at runtime via `StartRebind` — the next qualifying input
// becomes the new binding.
//
// Polling-based evaluation: `Map::Tick()` (called once per frame from
// game code, AFTER `Gamepad::Tick()`) reads the polled state of the
// attached window + connected gamepads and diffs against the action's
// previous state. No per-device event listeners — works regardless of
// gamepad connect/disconnect timing.
//
// Bindings store raw platform key codes (the same `KeyCode` domain as
// `Window::onRawKeyDown`). Source code uses `Key(CrafterKeys::Space)`
// from :Keys to obtain cross-platform default codes at compile time.
// Binding files serialized with `BindingToString` are platform-specific
// (a Win32 save won't load on Wayland) — document this for callers.
export namespace Crafter::Input {
enum class ActionType : std::uint8_t {
Button, // digital — pressed/released
Axis1D, // -1..1 (or 0..1 for triggers / scroll)
Vector2, // 2D — sticks, WASD, mouse delta
};
// ─── Binding alternatives ───────────────────────────────────────
struct KeyBind { KeyCode code; };
struct MouseButtonBind { std::uint8_t button; }; // 0 = left, 1 = right
struct MouseScrollBind { }; // Axis1D, accumulator drained per tick
struct MouseDeltaBind { float scale = 1.0f; }; // Vector2
struct GamepadButtonBind { std::uint32_t gamepadId; Gamepad::Button button; };
struct GamepadAxisBind { std::uint32_t gamepadId; Gamepad::Axis axis; bool invert = false; };
struct GamepadStickBind { std::uint32_t gamepadId; Gamepad::Stick stick; }; // Vector2
struct WASDBind {
KeyCode up;
KeyCode down;
KeyCode left;
KeyCode right;
};
using Binding = std::variant<
KeyBind, MouseButtonBind, MouseScrollBind, MouseDeltaBind,
GamepadButtonBind, GamepadAxisBind, GamepadStickBind, WASDBind
>;
struct Action {
std::string name;
ActionType type;
std::vector<Binding> bindings;
// Analog deadzone — bindings below this magnitude read as zero.
// Applied per-axis to GamepadAxisBind, radially to
// GamepadStickBind. Doesn't apply to Button (digital).
float deadzone = 0.15f;
// Polled state. Read freely from the listener callback or
// anywhere else; updated by `Map::Tick()`.
bool pressed = false;
float value = 0.0f;
Vector<float, 2> vector2{};
// Events fired during `Map::Tick()` when the polled state crosses
// an edge or changes value.
Event<void> onPerformed; // pressed (Button) / non-zero edge (axis)
Event<void> onCanceled; // released (Button) / back to zero
Event<float> onValueChanged; // Axis1D — value
Event<Vector<float,2>> onVector2Changed; // Vector2 — vector2
};
enum class CaptureMask : std::uint8_t {
Keyboard = 1,
Mouse = 2,
Gamepad = 4,
Any = 0xFF,
};
constexpr CaptureMask operator|(CaptureMask a, CaptureMask b) {
return (CaptureMask)((std::uint8_t)a | (std::uint8_t)b);
}
constexpr bool HasFlag(CaptureMask m, CaptureMask f) {
return ((std::uint8_t)m & (std::uint8_t)f) != 0;
}
struct Map {
std::vector<std::unique_ptr<Action>> actions;
Action& AddAction(std::string name, ActionType type);
Action* Find(std::string_view name);
// Subscribe internally to the window's mouse-scroll event so a
// single scroll between ticks isn't lost. All other state is
// polled from `Window` / `Gamepad`. Detaching releases that one
// listener; the polled-state reads automatically stop.
void Attach(Window& window);
void Detach();
// Apply current input state to all actions. Fire edge / value
// events. Call once per frame AFTER `Gamepad::Tick()`. Safe to
// call when not attached (no-op).
void Tick();
// Capture the next qualifying input as a binding. While capture
// is active, that input does NOT dispatch to actions — it only
// routes to `onCaptured`. Capturing replaces nothing in the
// action's bindings; the callback decides whether to assign,
// append, or discard. Cancel via `StopRebind`.
void StartRebind(Action& action, CaptureMask mask,
std::function<void(Binding)> onCaptured);
void StopRebind();
bool IsRebinding() const;
// ─── Internals (exposed because we're a POD-ish struct) ──────
Window* window = nullptr;
// Mouse scroll is the one event-driven source; everything else
// is polled. The listener feeds into `scrollAccumulator`,
// drained on the next Tick.
std::unique_ptr<EventListener<std::uint32_t>> scrollListener;
std::int32_t scrollAccumulator = 0;
// Mouse delta is computed from window.currentMousePos vs the
// position observed last tick. nullopt before first tick.
std::optional<Vector<float, 2>> lastMousePos;
// Rebind state.
struct RebindState {
Action* action;
CaptureMask mask;
std::function<void(Binding)> onCaptured;
// Snapshot of all input at rebind-start, so we only fire on
// a fresh down-edge (not on a button held since before).
std::unordered_set<KeyCode> keysHeldAtStart;
bool mouseLeftHeldAtStart = false;
bool mouseRightHeldAtStart = false;
std::unordered_map<std::uint32_t, std::array<bool, (std::size_t)Gamepad::Button::Max>> gamepadButtonsAtStart;
};
std::optional<RebindState> rebind;
};
// ─── Serialization ──────────────────────────────────────────────
// Single-binding text round-trip. Caller composes binding files
// however they like (JSON / INI / line-per-binding). Formats:
// "key:<hex>" — KeyBind
// "mb:<0|1>" — MouseButtonBind
// "mscroll" — MouseScrollBind
// "mdelta:<scale>" — MouseDeltaBind
// "gpb:<id>:<button>" — GamepadButtonBind
// "gpa:<id>:<axis>:<inv>" — GamepadAxisBind (inv = 0/1)
// "gps:<id>:<stick>" — GamepadStickBind
// "wasd:<u>:<d>:<l>:<r>" — WASDBind, four hex KeyCodes
// KeyCode hex values are platform-specific; see :Keys.
std::string BindingToString(const Binding&);
std::optional<Binding> BindingFromString(std::string_view);
}

View file

@ -20,6 +20,7 @@ module;
export module Crafter.Graphics:InputField;
import std;
import :Types;
import :Keys;
import :Font;
import :UI;
import :UIComponents;
@ -81,8 +82,12 @@ export namespace Crafter {
void InputField_OnText(InputField&, std::string_view utf8);
// Edit-control keys: Backspace, Delete, Left, Right, Home, End. Anything
// else is ignored. Safe to feed every key the host receives.
void InputField_OnKey(InputField&, CrafterKeys);
// else is ignored. Safe to feed every key the host receives. The KeyCode
// is the raw platform key (Win32 PS/2 scancode + extended bit; Wayland
// kernel keycode) — typically the value delivered by
// `Window::onRawKeyDown`. Internal comparisons use `Key(CrafterKeys::X)`
// from :Keys so the code stays cross-platform.
void InputField_OnKey(InputField&, KeyCode);
// Map a click x-coord (in window pixels) to a cursor position. `rect` is
// the field's current draw rect; `colors.paddingX` is consulted for the

View file

@ -0,0 +1,331 @@
/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
Catcrafts.net
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3.0 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
module;
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
#include <linux/input-event-codes.h>
#endif
export module Crafter.Graphics:Keys;
import std;
import :Types;
// Compile-time translation from the abstract `CrafterKeys` enum to the raw
// platform key code that the runtime actually stores and compares. The whole
// point: bindings, events, and InputField never carry the abstract enum at
// runtime — they carry the raw integer the platform delivers. `Key(...)` is
// `consteval` so every call from source folds to an immediate operand; user
// code stays cross-platform (`Key(CrafterKeys::Space)`) without any `#ifdef`
// at the call site.
//
// Domain of the returned KeyCode:
// Win32: bits 0..7 = PS/2 set-1 scancode byte. Bit 8 = extended-key flag
// (the 0xE0-prefixed variants — RightCtrl, RightAlt, the keypad's
// Enter and /, the cursor cluster's arrows/Home/End/etc., the
// Windows keys). At runtime, WndProc OR-s in 0x100 when lParam
// bit 24 is set, so a saved binding for "RightCtrl" (0x11D) survives
// round-trip with the actual hardware event.
// Wayland/Linux: kernel input-event-codes (KEY_*) directly. The +8 X11
// offset is stripped at the wl_keyboard.key boundary so the
// runtime values match the table here.
//
// Keys with no clean scancode encoding (Pause, some keys without standard
// physical position) return 0. 0 is documented as "unmapped"; binding to it
// will never match a real keypress.
export namespace Crafter {
using KeyCode = std::uint32_t;
consteval KeyCode Key(CrafterKeys k) {
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
// PS/2 set-1 scancodes. Extended keys carry 0x100 so the runtime
// path can disambiguate by OR-ing in 0x100 when lParam bit 24 is set.
switch (k) {
// Alphabetic
case CrafterKeys::A: return 0x1E;
case CrafterKeys::B: return 0x30;
case CrafterKeys::C: return 0x2E;
case CrafterKeys::D: return 0x20;
case CrafterKeys::E: return 0x12;
case CrafterKeys::F: return 0x21;
case CrafterKeys::G: return 0x22;
case CrafterKeys::H: return 0x23;
case CrafterKeys::I: return 0x17;
case CrafterKeys::J: return 0x24;
case CrafterKeys::K: return 0x25;
case CrafterKeys::L: return 0x26;
case CrafterKeys::M: return 0x32;
case CrafterKeys::N: return 0x31;
case CrafterKeys::O: return 0x18;
case CrafterKeys::P: return 0x19;
case CrafterKeys::Q: return 0x10;
case CrafterKeys::R: return 0x13;
case CrafterKeys::S: return 0x1F;
case CrafterKeys::T: return 0x14;
case CrafterKeys::U: return 0x16;
case CrafterKeys::V: return 0x2F;
case CrafterKeys::W: return 0x11;
case CrafterKeys::X: return 0x2D;
case CrafterKeys::Y: return 0x15;
case CrafterKeys::Z: return 0x2C;
// Numeric (top row)
case CrafterKeys::_1: return 0x02;
case CrafterKeys::_2: return 0x03;
case CrafterKeys::_3: return 0x04;
case CrafterKeys::_4: return 0x05;
case CrafterKeys::_5: return 0x06;
case CrafterKeys::_6: return 0x07;
case CrafterKeys::_7: return 0x08;
case CrafterKeys::_8: return 0x09;
case CrafterKeys::_9: return 0x0A;
case CrafterKeys::_0: return 0x0B;
// Function keys
case CrafterKeys::F1: return 0x3B;
case CrafterKeys::F2: return 0x3C;
case CrafterKeys::F3: return 0x3D;
case CrafterKeys::F4: return 0x3E;
case CrafterKeys::F5: return 0x3F;
case CrafterKeys::F6: return 0x40;
case CrafterKeys::F7: return 0x41;
case CrafterKeys::F8: return 0x42;
case CrafterKeys::F9: return 0x43;
case CrafterKeys::F10: return 0x44;
case CrafterKeys::F11: return 0x57;
case CrafterKeys::F12: return 0x58;
// Control keys
case CrafterKeys::Escape: return 0x01;
case CrafterKeys::Tab: return 0x0F;
case CrafterKeys::Enter: return 0x1C;
case CrafterKeys::Space: return 0x39;
case CrafterKeys::Backspace: return 0x0E;
case CrafterKeys::Delete: return 0x153; // extended
case CrafterKeys::Insert: return 0x152; // extended
case CrafterKeys::Home: return 0x147; // extended
case CrafterKeys::End: return 0x14F; // extended
case CrafterKeys::PageUp: return 0x149; // extended
case CrafterKeys::PageDown: return 0x151; // extended
case CrafterKeys::CapsLock: return 0x3A;
case CrafterKeys::NumLock: return 0x45;
case CrafterKeys::ScrollLock: return 0x46;
// Modifiers
case CrafterKeys::LeftShift: return 0x2A;
case CrafterKeys::RightShift: return 0x36;
case CrafterKeys::LeftCtrl: return 0x1D;
case CrafterKeys::RightCtrl: return 0x11D; // extended
case CrafterKeys::LeftAlt: return 0x38;
case CrafterKeys::RightAlt: return 0x138; // extended
case CrafterKeys::LeftSuper: return 0x15B; // extended
case CrafterKeys::RightSuper: return 0x15C; // extended
// Arrows (all extended on Win32 — the cursor cluster, not the keypad)
case CrafterKeys::Up: return 0x148;
case CrafterKeys::Down: return 0x150;
case CrafterKeys::Left: return 0x14B;
case CrafterKeys::Right: return 0x14D;
// Keypad
case CrafterKeys::keypad_0: return 0x52;
case CrafterKeys::keypad_1: return 0x4F;
case CrafterKeys::keypad_2: return 0x50;
case CrafterKeys::keypad_3: return 0x51;
case CrafterKeys::keypad_4: return 0x4B;
case CrafterKeys::keypad_5: return 0x4C;
case CrafterKeys::keypad_6: return 0x4D;
case CrafterKeys::keypad_7: return 0x47;
case CrafterKeys::keypad_8: return 0x48;
case CrafterKeys::keypad_9: return 0x49;
case CrafterKeys::keypad_enter: return 0x11C; // extended
case CrafterKeys::keypad_plus: return 0x4E;
case CrafterKeys::keypad_minus: return 0x4A;
case CrafterKeys::keypad_multiply: return 0x37;
case CrafterKeys::keypad_divide: return 0x135; // extended
case CrafterKeys::keypad_decimal: return 0x53;
// Punctuation
case CrafterKeys::grave: return 0x29;
case CrafterKeys::minus: return 0x0C;
case CrafterKeys::equal: return 0x0D;
case CrafterKeys::bracket_left: return 0x1A;
case CrafterKeys::bracket_right: return 0x1B;
case CrafterKeys::backslash: return 0x2B;
case CrafterKeys::semicolon: return 0x27;
case CrafterKeys::quote: return 0x28;
case CrafterKeys::comma: return 0x33;
case CrafterKeys::period: return 0x34;
case CrafterKeys::slash: return 0x35;
case CrafterKeys::print_screen: return 0x137; // extended
case CrafterKeys::pause: return 0; // unmapped — multi-byte sequence
case CrafterKeys::menu: return 0x15D; // extended (App key)
// Multimedia / browser / launch keys (all extended on Win32)
case CrafterKeys::volume_up: return 0x130;
case CrafterKeys::volume_down: return 0x12E;
case CrafterKeys::volume_mute: return 0x120;
case CrafterKeys::media_play: return 0x122;
case CrafterKeys::media_stop: return 0x124;
case CrafterKeys::media_prev: return 0x110;
case CrafterKeys::media_next: return 0x119;
case CrafterKeys::browser_back: return 0x16A;
case CrafterKeys::browser_forward: return 0x169;
case CrafterKeys::browser_refresh: return 0x167;
case CrafterKeys::browser_stop: return 0x168;
case CrafterKeys::browser_search: return 0x165;
case CrafterKeys::browser_home: return 0x132;
case CrafterKeys::launch_mail: return 0x16C;
case CrafterKeys::launch_calculator: return 0x121;
case CrafterKeys::launch_media_player: return 0x16D;
case CrafterKeys::CrafterKeysMax: return 0;
}
#endif
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
// Linux kernel input-event-codes. The Wayland keyboard handler strips
// the +8 X11 offset before delivery so these match wl_keyboard.key
// values directly.
switch (k) {
// Alphabetic
case CrafterKeys::A: return KEY_A;
case CrafterKeys::B: return KEY_B;
case CrafterKeys::C: return KEY_C;
case CrafterKeys::D: return KEY_D;
case CrafterKeys::E: return KEY_E;
case CrafterKeys::F: return KEY_F;
case CrafterKeys::G: return KEY_G;
case CrafterKeys::H: return KEY_H;
case CrafterKeys::I: return KEY_I;
case CrafterKeys::J: return KEY_J;
case CrafterKeys::K: return KEY_K;
case CrafterKeys::L: return KEY_L;
case CrafterKeys::M: return KEY_M;
case CrafterKeys::N: return KEY_N;
case CrafterKeys::O: return KEY_O;
case CrafterKeys::P: return KEY_P;
case CrafterKeys::Q: return KEY_Q;
case CrafterKeys::R: return KEY_R;
case CrafterKeys::S: return KEY_S;
case CrafterKeys::T: return KEY_T;
case CrafterKeys::U: return KEY_U;
case CrafterKeys::V: return KEY_V;
case CrafterKeys::W: return KEY_W;
case CrafterKeys::X: return KEY_X;
case CrafterKeys::Y: return KEY_Y;
case CrafterKeys::Z: return KEY_Z;
// Numeric
case CrafterKeys::_0: return KEY_0;
case CrafterKeys::_1: return KEY_1;
case CrafterKeys::_2: return KEY_2;
case CrafterKeys::_3: return KEY_3;
case CrafterKeys::_4: return KEY_4;
case CrafterKeys::_5: return KEY_5;
case CrafterKeys::_6: return KEY_6;
case CrafterKeys::_7: return KEY_7;
case CrafterKeys::_8: return KEY_8;
case CrafterKeys::_9: return KEY_9;
// Function keys
case CrafterKeys::F1: return KEY_F1;
case CrafterKeys::F2: return KEY_F2;
case CrafterKeys::F3: return KEY_F3;
case CrafterKeys::F4: return KEY_F4;
case CrafterKeys::F5: return KEY_F5;
case CrafterKeys::F6: return KEY_F6;
case CrafterKeys::F7: return KEY_F7;
case CrafterKeys::F8: return KEY_F8;
case CrafterKeys::F9: return KEY_F9;
case CrafterKeys::F10: return KEY_F10;
case CrafterKeys::F11: return KEY_F11;
case CrafterKeys::F12: return KEY_F12;
// Control keys
case CrafterKeys::Escape: return KEY_ESC;
case CrafterKeys::Tab: return KEY_TAB;
case CrafterKeys::Enter: return KEY_ENTER;
case CrafterKeys::Space: return KEY_SPACE;
case CrafterKeys::Backspace: return KEY_BACKSPACE;
case CrafterKeys::Delete: return KEY_DELETE;
case CrafterKeys::Insert: return KEY_INSERT;
case CrafterKeys::Home: return KEY_HOME;
case CrafterKeys::End: return KEY_END;
case CrafterKeys::PageUp: return KEY_PAGEUP;
case CrafterKeys::PageDown: return KEY_PAGEDOWN;
case CrafterKeys::CapsLock: return KEY_CAPSLOCK;
case CrafterKeys::NumLock: return KEY_NUMLOCK;
case CrafterKeys::ScrollLock: return KEY_SCROLLLOCK;
// Modifiers
case CrafterKeys::LeftShift: return KEY_LEFTSHIFT;
case CrafterKeys::RightShift: return KEY_RIGHTSHIFT;
case CrafterKeys::LeftCtrl: return KEY_LEFTCTRL;
case CrafterKeys::RightCtrl: return KEY_RIGHTCTRL;
case CrafterKeys::LeftAlt: return KEY_LEFTALT;
case CrafterKeys::RightAlt: return KEY_RIGHTALT;
case CrafterKeys::LeftSuper: return KEY_LEFTMETA;
case CrafterKeys::RightSuper: return KEY_RIGHTMETA;
// Arrows
case CrafterKeys::Up: return KEY_UP;
case CrafterKeys::Down: return KEY_DOWN;
case CrafterKeys::Left: return KEY_LEFT;
case CrafterKeys::Right: return KEY_RIGHT;
// Keypad
case CrafterKeys::keypad_0: return KEY_KP0;
case CrafterKeys::keypad_1: return KEY_KP1;
case CrafterKeys::keypad_2: return KEY_KP2;
case CrafterKeys::keypad_3: return KEY_KP3;
case CrafterKeys::keypad_4: return KEY_KP4;
case CrafterKeys::keypad_5: return KEY_KP5;
case CrafterKeys::keypad_6: return KEY_KP6;
case CrafterKeys::keypad_7: return KEY_KP7;
case CrafterKeys::keypad_8: return KEY_KP8;
case CrafterKeys::keypad_9: return KEY_KP9;
case CrafterKeys::keypad_enter: return KEY_KPENTER;
case CrafterKeys::keypad_plus: return KEY_KPPLUS;
case CrafterKeys::keypad_minus: return KEY_KPMINUS;
case CrafterKeys::keypad_multiply: return KEY_KPASTERISK;
case CrafterKeys::keypad_divide: return KEY_KPSLASH;
case CrafterKeys::keypad_decimal: return KEY_KPDOT;
// Punctuation
case CrafterKeys::grave: return KEY_GRAVE;
case CrafterKeys::minus: return KEY_MINUS;
case CrafterKeys::equal: return KEY_EQUAL;
case CrafterKeys::bracket_left: return KEY_LEFTBRACE;
case CrafterKeys::bracket_right: return KEY_RIGHTBRACE;
case CrafterKeys::backslash: return KEY_BACKSLASH;
case CrafterKeys::semicolon: return KEY_SEMICOLON;
case CrafterKeys::quote: return KEY_APOSTROPHE;
case CrafterKeys::comma: return KEY_COMMA;
case CrafterKeys::period: return KEY_DOT;
case CrafterKeys::slash: return KEY_SLASH;
case CrafterKeys::print_screen: return KEY_SYSRQ;
case CrafterKeys::pause: return KEY_PAUSE;
case CrafterKeys::menu: return KEY_COMPOSE;
// Multimedia / browser / launch keys
case CrafterKeys::volume_up: return KEY_VOLUMEUP;
case CrafterKeys::volume_down: return KEY_VOLUMEDOWN;
case CrafterKeys::volume_mute: return KEY_MUTE;
case CrafterKeys::media_play: return KEY_PLAYPAUSE;
case CrafterKeys::media_stop: return KEY_STOPCD;
case CrafterKeys::media_prev: return KEY_PREVIOUSSONG;
case CrafterKeys::media_next: return KEY_NEXTSONG;
case CrafterKeys::browser_back: return KEY_BACK;
case CrafterKeys::browser_forward: return KEY_FORWARD;
case CrafterKeys::browser_refresh: return KEY_REFRESH;
case CrafterKeys::browser_stop: return KEY_STOP;
case CrafterKeys::browser_search: return KEY_SEARCH;
case CrafterKeys::browser_home: return KEY_HOMEPAGE;
case CrafterKeys::launch_mail: return KEY_MAIL;
case CrafterKeys::launch_calculator: return KEY_CALC;
case CrafterKeys::launch_media_player: return KEY_MEDIA;
case CrafterKeys::CrafterKeysMax: return 0;
}
#endif
return 0;
}
}

View file

@ -24,6 +24,7 @@ module;
export module Crafter.Graphics:Mesh;
import std;
import Crafter.Math;
import Crafter.Asset;
import :VulkanBuffer;
export namespace Crafter {
@ -33,11 +34,25 @@ export namespace Crafter {
VulkanBuffer<char, false> blasBuffer;
VulkanBuffer<Vector<float, 3, 3>, true> vertexBuffer;
VulkanBuffer<std::uint32_t, true> indexBuffer;
// Lives until the cmd buffer issued by the compressed Build path
// completes execution. Kept as a member so the recorded
// vkCmdDecompressMemoryEXT references valid memory until the queue
// submit signals — caller must not re-Build or destroy the Mesh
// before that submit's fence is signaled (same contract as the
// existing uncompressed path).
VulkanBuffer<std::byte, true> compressedStaging;
VkAccelerationStructureGeometryTrianglesDataKHR blasData;
VkAccelerationStructureGeometryKHR blas;
VkAccelerationStructureKHR accelerationStructure;
VkDeviceAddress blasAddr;
bool opaque;
void Build(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, VkCommandBuffer cmd);
// GPU path: decompresses vertex (region 0) and index (region 1) streams
// from asset.blob into vertexBuffer / indexBuffer using
// VK_EXT_memory_decompression. Falls back to CPU decode + the
// uncompressed Build if Device::memoryDecompressionSupported is false.
// Region 2 (data) is not consumed here — the caller decompresses it
// into their own buffer if needed (or uses Compression::DecompressCPU).
void Build(const ::Crafter::CompressedMeshAsset& asset, VkCommandBuffer cmd);
};
}

View file

@ -274,6 +274,12 @@ export namespace Crafter {
bool firstDispatchThisFrame_ = true;
// Subscription to window.onResize. Each resize destroys the old
// swapchain images, so the per-frame heap entries we wrote at
// outImageSlot_ now reference dangling VkImage handles. The
// listener re-writes them and flushes the descriptor heaps.
Crafter::EventListener<void> resizeSub_;
void WriteSwapchainDescriptors();
void WriteFontAtlasDescriptor();

View file

@ -52,12 +52,20 @@ namespace Crafter {
class VulkanBuffer : public VulkanBufferBase, public VulkanBufferMappedConditional<T, Mapped> {
public:
VulkanBuffer() = default;
void Create(VkBufferUsageFlags usageFlags, VkMemoryPropertyFlags memoryPropertyFlags, std::uint32_t count) {
void Create(VkBufferUsageFlags2 usageFlags, VkMemoryPropertyFlags memoryPropertyFlags, std::uint32_t count) {
size = count * sizeof(T);
// Carry usage in the maintenance5 flags2 chain so 64-bit bits
// (e.g. VK_BUFFER_USAGE_2_MEMORY_DECOMPRESSION_BIT_EXT, bit 35)
// are not truncated.
VkBufferUsageFlags2CreateInfo usageFlags2 {
.sType = VK_STRUCTURE_TYPE_BUFFER_USAGE_FLAGS_2_CREATE_INFO,
.usage = usageFlags,
};
VkBufferCreateInfo bufferCreateInfo {};
bufferCreateInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufferCreateInfo.usage = usageFlags;
bufferCreateInfo.pNext = &usageFlags2;
bufferCreateInfo.usage = 0;
bufferCreateInfo.size = sizeof(T)*count;
Device::CheckVkResult(vkCreateBuffer(Device::device, &bufferCreateInfo, nullptr, &buffer));
@ -98,7 +106,7 @@ namespace Crafter {
buffer = VK_NULL_HANDLE;
}
void Resize(VkBufferUsageFlags usageFlags, VkMemoryPropertyFlags memoryPropertyFlags, std::uint32_t count) {
void Resize(VkBufferUsageFlags2 usageFlags, VkMemoryPropertyFlags memoryPropertyFlags, std::uint32_t count) {
if(buffer != VK_NULL_HANDLE) {
Clear();
}

View file

@ -46,6 +46,7 @@ module;
export module Crafter.Graphics:Window;
import std;
import :Types;
import :Keys;
import Crafter.Event;
export namespace Crafter {
@ -66,15 +67,21 @@ export namespace Crafter {
Event<void> onClose;
Event<void> onBeforeUpdate;
Event<FrameTime> onUpdate;
// Fires when the swapchain has been recreated for a new size.
// width/height already reflect the new size. Passes that hold
// descriptors referring to window.imageViews[] must re-write them
// here (the old VkImage handles have been destroyed).
Event<void> onResize;
bool open = true;
bool updating = false;
bool heldkeys[static_cast<std::uint32_t>(CrafterKeys::CrafterKeysMax)] = {};
Event<void> onKeyDown[static_cast<std::uint32_t>(CrafterKeys::CrafterKeysMax)];
Event<void> onKeyHold[static_cast<std::uint32_t>(CrafterKeys::CrafterKeysMax)];
Event<void> onKeyUp[static_cast<std::uint32_t>(CrafterKeys::CrafterKeysMax)];
Event<CrafterKeys> onAnyKeyDown;
Event<CrafterKeys> onAnyKeyHold;
Event<CrafterKeys> onAnyKeyUp;
// Currently-pressed raw key codes. The runtime stores raw platform
// codes only (Win32 PS/2 scancode + extended bit; Wayland kernel
// keycode). Cross-platform default bindings use `Key(CrafterKeys::X)`
// from :Keys to obtain the right code at compile time.
std::unordered_set<KeyCode> heldKeys;
Event<KeyCode> onRawKeyDown;
Event<KeyCode> onRawKeyHold;
Event<KeyCode> onRawKeyUp;
Event<const std::string_view> onTextInput;
Event<void> onMouseRightClick;
Event<void> onMouseLeftClick;
@ -141,6 +148,11 @@ export namespace Crafter {
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
float scale = 1.0f;
bool configured = false;
// Pending size from the most recent xdg_toplevel.configure, in
// surface-local (logical DP) units. Applied on xdg_surface.configure
// via Resize(); 0 means "compositor has no preference, keep current".
std::int32_t pendingLogicalWidth = 0;
std::int32_t pendingLogicalHeight = 0;
xdg_toplevel* xdgToplevel = nullptr;
wp_viewport* wpViewport = nullptr;
wl_surface* surface = nullptr;
@ -186,6 +198,12 @@ export namespace Crafter {
VkCommandBuffer GetCmd();
void EndCmd(VkCommandBuffer cmd);
void CreateSwapchain();
// Tear-and-rebuild helper used by Resize() and the OUT_OF_DATE
// recovery in Render(). Calls CreateSwapchain() and re-issues the
// initial PRESENT_SRC_KHR layout transition for the new images so
// Render()'s barriers (which assume oldLayout = PRESENT_SRC_KHR)
// stay valid. Does NOT fire onResize — callers do that.
void RecreateSwapchainAndImages();
// Save the current swapchain image (state after Render() returns) to
// a PNG file. Allocates a one-shot staging buffer + command buffer,

View file

@ -20,8 +20,11 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
export module Crafter.Graphics;
export import :Window;
export import :Types;
export import :Keys;
export import :Gamepad;
export import :Window;
export import :Input;
export import :Device;
export import :Font;
export import :Animation;
@ -42,4 +45,6 @@ export import :FontAtlas;
export import :ComputeShader;
export import :UI;
export import :UIComponents;
export import :InputField;
export import :InputField;
export import :Clipboard;
export import :Decompress;