new input system
This commit is contained in:
parent
b3db40ebec
commit
ac2eb7fb0a
31 changed files with 3292 additions and 781 deletions
BIN
examples/Decompression/checker.ctex
Normal file
BIN
examples/Decompression/checker.ctex
Normal file
Binary file not shown.
BIN
examples/Decompression/cube.cmesh
Normal file
BIN
examples/Decompression/cube.cmesh
Normal file
Binary file not shown.
189
examples/Decompression/main.cpp
Normal file
189
examples/Decompression/main.cpp
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
// End-to-end demo of GPU asset decompression via VK_EXT_memory_decompression.
|
||||
//
|
||||
// Walks the full compressed-asset pipeline:
|
||||
// 1. Build a procedural cube + checkerboard texture in memory.
|
||||
// 2. SaveCompressed → on-disk .cmesh / .ctex (GDeflate streams).
|
||||
// 3. LoadCompressed* → CompressedMeshAsset / CompressedTextureAsset.
|
||||
// 4. CPU verification: blob → DecompressCPU → byte-equal to source.
|
||||
// 5. GPU: Mesh::Build(compressedMesh, cmd) and ImageVulkan::Update(compressedTex, cmd, layout).
|
||||
// Picks the GPU path on NVIDIA (extension supported) or the CPU fallback elsewhere.
|
||||
//
|
||||
// No rendering — the goal is to exercise the asset pipeline and prove the
|
||||
// new APIs round-trip. Validation layers are enabled by Crafter.Graphics in
|
||||
// debug builds, so any VUID violation surfaces during FinishInit().
|
||||
|
||||
#include "vulkan/vulkan.h"
|
||||
|
||||
import Crafter.Graphics;
|
||||
import Crafter.Asset;
|
||||
import Crafter.Math;
|
||||
import std;
|
||||
|
||||
using namespace Crafter;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
|
||||
// Procedural unit cube centered at origin, scaled so it's visible if the
|
||||
// example is ever wired into a renderer. 8 unique positions, 36 indices.
|
||||
MeshAsset<void> MakeCubeMesh() {
|
||||
MeshAsset<void> mesh;
|
||||
mesh.vertexes = {
|
||||
{-50.f, -50.f, -50.f}, { 50.f, -50.f, -50.f},
|
||||
{ 50.f, 50.f, -50.f}, {-50.f, 50.f, -50.f},
|
||||
{-50.f, -50.f, 50.f}, { 50.f, -50.f, 50.f},
|
||||
{ 50.f, 50.f, 50.f}, {-50.f, 50.f, 50.f},
|
||||
};
|
||||
mesh.indexes = {
|
||||
// -Z
|
||||
0, 1, 2, 0, 2, 3,
|
||||
// +Z
|
||||
4, 6, 5, 4, 7, 6,
|
||||
// -X
|
||||
0, 3, 7, 0, 7, 4,
|
||||
// +X
|
||||
1, 5, 6, 1, 6, 2,
|
||||
// -Y
|
||||
0, 4, 5, 0, 5, 1,
|
||||
// +Y
|
||||
3, 2, 6, 3, 6, 7,
|
||||
};
|
||||
return mesh;
|
||||
}
|
||||
|
||||
struct RGBA8 { std::uint8_t r, g, b, a; };
|
||||
|
||||
// 256×256 checkerboard with smooth radial fade — compressible enough to make
|
||||
// the GDeflate ratio interesting, structured enough that the demo is
|
||||
// meaningful.
|
||||
TextureAsset<RGBA8> MakeCheckerboard() {
|
||||
TextureAsset<RGBA8> tex;
|
||||
tex.sizeX = 256;
|
||||
tex.sizeY = 256;
|
||||
tex.opaque = OpaqueType::FullyOpaque;
|
||||
tex.pixels.resize(tex.sizeX * tex.sizeY);
|
||||
for (std::uint32_t y = 0; y < tex.sizeY; ++y) {
|
||||
for (std::uint32_t x = 0; x < tex.sizeX; ++x) {
|
||||
bool checker = ((x / 32) ^ (y / 32)) & 1;
|
||||
float dx = float(x) - 128.0f;
|
||||
float dy = float(y) - 128.0f;
|
||||
float d = std::sqrt(dx * dx + dy * dy) / 180.0f;
|
||||
float fade = std::clamp(1.0f - d, 0.0f, 1.0f);
|
||||
std::uint8_t base = checker ? 220 : 40;
|
||||
std::uint8_t lit = static_cast<std::uint8_t>(base * fade + 16);
|
||||
tex.pixels[y * tex.sizeX + x] = RGBA8{lit, lit, lit, 255};
|
||||
}
|
||||
}
|
||||
return tex;
|
||||
}
|
||||
|
||||
double Ratio(std::size_t a, std::size_t b) {
|
||||
return b == 0 ? 0.0 : 100.0 * double(a) / double(b);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const fs::path meshPath = "cube.cmesh";
|
||||
const fs::path texPath = "checker.ctex";
|
||||
|
||||
// ── 1. Build procedural assets ─────────────────────────────────────
|
||||
MeshAsset<void> srcMesh = MakeCubeMesh();
|
||||
TextureAsset<RGBA8> srcTex = MakeCheckerboard();
|
||||
|
||||
const std::size_t srcMeshBytes =
|
||||
srcMesh.vertexes.size() * sizeof(srcMesh.vertexes[0])
|
||||
+ srcMesh.indexes.size() * sizeof(srcMesh.indexes[0]);
|
||||
const std::size_t srcTexBytes =
|
||||
srcTex.pixels.size() * sizeof(RGBA8);
|
||||
|
||||
std::println("Procedural cube: {} vertices, {} indices ({} bytes)",
|
||||
srcMesh.vertexes.size(), srcMesh.indexes.size(), srcMeshBytes);
|
||||
std::println("Procedural checker: {}x{} RGBA8 ({} bytes)",
|
||||
srcTex.sizeX, srcTex.sizeY, srcTexBytes);
|
||||
|
||||
// ── 2. SaveCompressed ──────────────────────────────────────────────
|
||||
srcMesh.SaveCompressed(meshPath);
|
||||
srcTex.SaveCompressed(texPath);
|
||||
|
||||
const std::size_t meshFileSize = fs::file_size(meshPath);
|
||||
const std::size_t texFileSize = fs::file_size(texPath);
|
||||
std::println("Saved {}: {} bytes ({:.1f}% of raw)",
|
||||
meshPath.string(), meshFileSize, Ratio(meshFileSize, srcMeshBytes));
|
||||
std::println("Saved {}: {} bytes ({:.1f}% of raw)",
|
||||
texPath.string(), texFileSize, Ratio(texFileSize, srcTexBytes));
|
||||
|
||||
// ── 3. LoadCompressed ──────────────────────────────────────────────
|
||||
CompressedMeshAsset loadedMesh = LoadCompressedMesh(meshPath);
|
||||
CompressedTextureAsset loadedTex = LoadCompressedTexture(texPath);
|
||||
if (loadedMesh.vertexCount != srcMesh.vertexes.size()
|
||||
|| loadedMesh.indexCount != srcMesh.indexes.size()) {
|
||||
std::println(std::cerr,"[FAIL] mesh header mismatch after LoadCompressedMesh");
|
||||
return 1;
|
||||
}
|
||||
if (loadedTex.sizeX != srcTex.sizeX || loadedTex.sizeY != srcTex.sizeY) {
|
||||
std::println(std::cerr,"[FAIL] texture header mismatch after LoadCompressedTexture");
|
||||
return 1;
|
||||
}
|
||||
std::println("Loaded headers OK.");
|
||||
|
||||
// ── 4. CPU roundtrip verification ──────────────────────────────────
|
||||
{
|
||||
std::vector<Vector<float, 3, 3>> v(loadedMesh.vertexCount);
|
||||
std::vector<std::uint32_t> i(loadedMesh.indexCount);
|
||||
std::array<std::span<std::byte>, 3> outputs = {
|
||||
std::as_writable_bytes(std::span(v)),
|
||||
std::as_writable_bytes(std::span(i)),
|
||||
std::span<std::byte>{},
|
||||
};
|
||||
Compression::DecompressCPU(loadedMesh.blob,
|
||||
std::span(outputs).first(loadedMesh.blob.regions.size()));
|
||||
if (v != srcMesh.vertexes || i != srcMesh.indexes) {
|
||||
std::println(std::cerr,"[FAIL] CPU mesh decompress != source");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
{
|
||||
std::vector<RGBA8> p(loadedTex.sizeX * loadedTex.sizeY);
|
||||
std::array<std::span<std::byte>, 1> outputs = {
|
||||
std::as_writable_bytes(std::span(p)),
|
||||
};
|
||||
Compression::DecompressCPU(loadedTex.blob, outputs);
|
||||
if (std::memcmp(p.data(), srcTex.pixels.data(), srcTexBytes) != 0) {
|
||||
std::println(std::cerr,"[FAIL] CPU texture decompress != source");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
std::println("CPU roundtrip OK (mesh + texture decode byte-equal).");
|
||||
|
||||
// ── 5. GPU path via Mesh::Build / ImageVulkan::Update ──────────────
|
||||
Device::Initialize();
|
||||
Window window(800, 600, "Decompression");
|
||||
|
||||
std::println("VK_EXT_memory_decompression: {}",
|
||||
Device::memoryDecompressionSupported ? "AVAILABLE → GPU path" : "absent → CPU fallback");
|
||||
|
||||
VkCommandBuffer cmd = window.StartInit();
|
||||
|
||||
DescriptorHeapVulkan heap;
|
||||
heap.Initialize(/*images*/ 1, /*buffers*/ 1, /*samplers*/ 0);
|
||||
window.descriptorHeap = &heap;
|
||||
|
||||
// Mesh cubeMesh;
|
||||
// cubeMesh.Build(loadedMesh, cmd);
|
||||
|
||||
ImageVulkan<RGBA8> checkerImage;
|
||||
checkerImage.Create(
|
||||
loadedTex.sizeX, loadedTex.sizeY, /*mipLevels*/ 1, cmd,
|
||||
VK_FORMAT_R8G8B8A8_UNORM,
|
||||
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||
checkerImage.Update(loadedTex, cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||
|
||||
window.FinishInit();
|
||||
std::println("GPU init submit + wait completed without validation errors.");
|
||||
//std::println("BLAS device address: 0x{:x}", cubeMesh.blasAddr);
|
||||
|
||||
// Cleanup happens via Window/Device dtors when they go out of scope.
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -11,15 +11,13 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
|
||||
Configuration cfg;
|
||||
cfg.path = "./";
|
||||
cfg.name = "OptionsSpike";
|
||||
cfg.outputName = "OptionsSpike";
|
||||
cfg.name = "Decompression";
|
||||
cfg.outputName = "Decompression";
|
||||
ApplyStandardArgs(cfg, args);
|
||||
cfg.dependencies = { graphics };
|
||||
|
||||
std::array<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "main" };
|
||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||
|
||||
cfg.files.push_back("font.ttf");
|
||||
return cfg;
|
||||
}
|
||||
241
examples/InputSystem/main.cpp
Normal file
241
examples/InputSystem/main.cpp
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
// =====================================================================
|
||||
// InputSystem — guided tour of Crafter::Input
|
||||
// =====================================================================
|
||||
//
|
||||
// Run this with a window focused. The console narrates each event as
|
||||
// it fires; the on-screen window only exists so the OS routes keyboard
|
||||
// and mouse input to us. Plug a gamepad in (Xbox / DualShock /
|
||||
// Switch Pro) and it will be picked up live.
|
||||
//
|
||||
// What this demo demonstrates, in order:
|
||||
//
|
||||
// 1. Three runtime layers stacked end-to-end:
|
||||
// - Crafter::Key(CrafterKeys::X) compile-time abstract → raw
|
||||
// platform KeyCode
|
||||
// - Window::onRawKeyDown raw keyboard channel for UI /
|
||||
// text input (NOT used for game
|
||||
// input — actions handle that)
|
||||
// - Crafter::Input::Map named actions → bindings →
|
||||
// events
|
||||
// - Crafter::Gamepad hot-plug, button + axis state,
|
||||
// rumble
|
||||
//
|
||||
// 2. All four action types: Button, Axis1D, Vector2, with multiple
|
||||
// bindings per action (any-of semantics — press Space OR press
|
||||
// gamepad-A; both trigger Jump).
|
||||
//
|
||||
// 3. Composite bindings: WASDBind (4 keys → Vector2),
|
||||
// GamepadStickBind (analog stick → Vector2), MouseDeltaBind
|
||||
// (mouse movement → Vector2).
|
||||
//
|
||||
// 4. Runtime rebinding: press R to enter rebind mode; the next key /
|
||||
// mouse button / gamepad button you press becomes the new Jump
|
||||
// binding. The captured input does NOT fire Jump that frame.
|
||||
//
|
||||
// 5. Serialization: bindings are round-tripped to text strings at
|
||||
// startup. Use these as the on-disk format for your settings file.
|
||||
//
|
||||
// 6. Hot-plug: connect / disconnect a gamepad while running. The
|
||||
// Map's polling-based evaluation seamlessly switches over —
|
||||
// bindings to a vanished gamepad become inert until reconnect.
|
||||
//
|
||||
// Controls:
|
||||
// Space / Gamepad A → Jump
|
||||
// WASD / Left stick → Move
|
||||
// Mouse-Left / Right Trigger → Fire
|
||||
// Mouse delta / Right stick → Look
|
||||
// Mouse wheel → Zoom (Axis1D)
|
||||
// R → Start rebinding "Jump" — press
|
||||
// any key/button next to assign it
|
||||
// Esc → Quit
|
||||
//
|
||||
// Output is intentionally chatty so you can SEE the system working.
|
||||
// In a real game, replace the print statements with your gameplay
|
||||
// code — that's it.
|
||||
// =====================================================================
|
||||
|
||||
import Crafter.Graphics;
|
||||
import Crafter.Event; // EventListener<>: Crafter.Graphics imports
|
||||
import Crafter.Math; // Vector<float,2>: it transitively, but
|
||||
// C++20 module-reachability rules require
|
||||
// us to name them explicitly to instantiate
|
||||
// the templates in this TU.
|
||||
import std;
|
||||
using namespace Crafter;
|
||||
using namespace Crafter::Input;
|
||||
|
||||
int main() {
|
||||
Device::Initialize();
|
||||
Window window(640, 360, "Crafter Input — give the window focus");
|
||||
|
||||
// =================================================================
|
||||
// Build the action map.
|
||||
//
|
||||
// AddAction returns a reference; we keep it so we can subscribe.
|
||||
// The map owns the actions (vector<unique_ptr>); references stay
|
||||
// valid for the map's lifetime.
|
||||
// =================================================================
|
||||
Map map;
|
||||
Action& jump = map.AddAction("Jump", ActionType::Button);
|
||||
Action& move = map.AddAction("Move", ActionType::Vector2);
|
||||
Action& fire = map.AddAction("Fire", ActionType::Button);
|
||||
Action& look = map.AddAction("Look", ActionType::Vector2);
|
||||
Action& zoom = map.AddAction("Zoom", ActionType::Axis1D);
|
||||
Action& reset = map.AddAction("Reset", ActionType::Button);
|
||||
Action& quit = map.AddAction("Quit", ActionType::Button);
|
||||
|
||||
// =================================================================
|
||||
// Default bindings.
|
||||
//
|
||||
// Key(CrafterKeys::X) is `consteval` — these literals fold to
|
||||
// per-platform scancodes at compile time, no runtime translation.
|
||||
// Multiple bindings per action are OR'd: pressing EITHER triggers
|
||||
// the action.
|
||||
//
|
||||
// gamepadId == 0 is a sentinel here meaning "first connected
|
||||
// gamepad". For multi-player you'd save the stable id from a
|
||||
// specific Device and use that.
|
||||
// =================================================================
|
||||
jump.bindings = {
|
||||
KeyBind{ Key(CrafterKeys::Space) },
|
||||
GamepadButtonBind{ 0, Gamepad::Button::South },
|
||||
};
|
||||
move.bindings = {
|
||||
WASDBind{
|
||||
Key(CrafterKeys::W), Key(CrafterKeys::S),
|
||||
Key(CrafterKeys::A), Key(CrafterKeys::D),
|
||||
},
|
||||
GamepadStickBind{ 0, Gamepad::Stick::Left },
|
||||
};
|
||||
fire.bindings = {
|
||||
MouseButtonBind{ 0 }, // left click
|
||||
GamepadAxisBind{ 0, Gamepad::Axis::RightTrigger, false }, // analog R2
|
||||
};
|
||||
look.bindings = {
|
||||
MouseDeltaBind{ 0.01f }, // scale down
|
||||
GamepadStickBind{ 0, Gamepad::Stick::Right },
|
||||
};
|
||||
zoom.bindings = {
|
||||
MouseScrollBind{},
|
||||
};
|
||||
reset.bindings = { KeyBind{ Key(CrafterKeys::R) } };
|
||||
quit.bindings = { KeyBind{ Key(CrafterKeys::Escape) } };
|
||||
|
||||
// =================================================================
|
||||
// Serialization round-trip — call sites can save these strings to
|
||||
// disk and load them back into Action::bindings later.
|
||||
//
|
||||
// IMPORTANT: KeyCode values are platform-specific (Win32 scancodes
|
||||
// vs. Wayland kernel keycodes). A saved file from one platform
|
||||
// does NOT load on the other. Store your config under platform-
|
||||
// specific paths.
|
||||
// =================================================================
|
||||
std::println("─── Default bindings (serialized form) ───");
|
||||
for (auto& a : map.actions) {
|
||||
for (auto& b : a->bindings) {
|
||||
std::string s = BindingToString(b);
|
||||
// Parse it back to prove the format roundtrips.
|
||||
auto parsed = BindingFromString(s);
|
||||
std::println(" {:<6} {} (roundtrip OK: {})",
|
||||
a->name, s, parsed.has_value());
|
||||
}
|
||||
}
|
||||
std::println("");
|
||||
|
||||
// =================================================================
|
||||
// Subscribe to action events.
|
||||
//
|
||||
// EventListeners use the same RAII pattern as the rest of the
|
||||
// library — declare them in scope, they unsubscribe when they go
|
||||
// out of scope. Capturing references to the Actions is safe
|
||||
// because the Map (and therefore its actions) outlive these
|
||||
// listeners (both are in main()'s scope).
|
||||
// =================================================================
|
||||
EventListener<void> jumpPerf(&jump.onPerformed,
|
||||
[] { std::println("[Jump] performed"); });
|
||||
EventListener<void> jumpCanc(&jump.onCanceled,
|
||||
[] { std::println("[Jump] canceled"); });
|
||||
|
||||
EventListener<Vector<float,2>> moveChanged(&move.onVector2Changed,
|
||||
[](Vector<float,2> v) {
|
||||
std::println("[Move] ({:+.2f}, {:+.2f})", v.x, v.y);
|
||||
});
|
||||
|
||||
EventListener<void> firePerf(&fire.onPerformed,
|
||||
[] { std::println("[Fire] performed"); });
|
||||
EventListener<void> fireCanc(&fire.onCanceled,
|
||||
[] { std::println("[Fire] canceled"); });
|
||||
|
||||
EventListener<Vector<float,2>> lookChanged(&look.onVector2Changed,
|
||||
[](Vector<float,2> v) {
|
||||
// Mouse delta fires every frame the mouse moves; throttle
|
||||
// by checking magnitude so the console doesn't drown.
|
||||
if (std::abs(v.x) + std::abs(v.y) > 0.01f) {
|
||||
std::println("[Look] ({:+.3f}, {:+.3f})", v.x, v.y);
|
||||
}
|
||||
});
|
||||
|
||||
EventListener<float> zoomChanged(&zoom.onValueChanged,
|
||||
[](float v) { std::println("[Zoom] delta {:+.0f}", v); });
|
||||
|
||||
EventListener<void> quitPerf(&quit.onPerformed,
|
||||
[&] {
|
||||
std::println("[Quit] shutting down");
|
||||
window.open = false;
|
||||
});
|
||||
|
||||
// =================================================================
|
||||
// Rebinding flow.
|
||||
//
|
||||
// Pressing R fires Reset → we call StartRebind for the Jump
|
||||
// action. While rebinding, the next qualifying input does NOT
|
||||
// fire Jump (or any other action that uses the same input). It
|
||||
// routes to the onCaptured callback exactly once.
|
||||
//
|
||||
// CaptureMask::Any accepts keyboard | mouse | gamepad. Narrow it
|
||||
// (Keyboard, Gamepad, etc.) to restrict what counts as a valid
|
||||
// binding for a given action.
|
||||
// =================================================================
|
||||
EventListener<void> resetPerf(&reset.onPerformed, [&] {
|
||||
std::println("[Rebind] Press any key / button to bind \"Jump\" "
|
||||
"(Esc to cancel)…");
|
||||
map.StartRebind(jump, CaptureMask::Any, [&](Binding captured) {
|
||||
jump.bindings.clear();
|
||||
jump.bindings.push_back(captured);
|
||||
std::println("[Rebind] Jump → {}", BindingToString(captured));
|
||||
});
|
||||
});
|
||||
|
||||
// =================================================================
|
||||
// Gamepad lifecycle observers.
|
||||
//
|
||||
// Gamepad::Tick() is called automatically inside Window::StartSync
|
||||
// every frame, so these events fire on the main thread alongside
|
||||
// every other event. Hot-plug a controller while running and you
|
||||
// should see the connect line below.
|
||||
// =================================================================
|
||||
EventListener<Gamepad::Device*> gpAdded(&Gamepad::onConnected,
|
||||
[](Gamepad::Device* d) {
|
||||
std::println("[Gamepad] connected: id={} name=\"{}\"",
|
||||
d->id, d->name);
|
||||
});
|
||||
EventListener<Gamepad::Device*> gpRemoved(&Gamepad::onDisconnected,
|
||||
[](Gamepad::Device* d) {
|
||||
std::println("[Gamepad] disconnected: id={}", d->id);
|
||||
});
|
||||
|
||||
// =================================================================
|
||||
// Connect the map to the window. Map polls window state + gamepad
|
||||
// state every Tick; we trigger Tick from onBeforeUpdate so it
|
||||
// runs after the OS event pump (and Gamepad::Tick) for that frame.
|
||||
// =================================================================
|
||||
map.Attach(window);
|
||||
EventListener<void> tick(&window.onBeforeUpdate, [&] { map.Tick(); });
|
||||
|
||||
std::println("─── Ready. Focus the window and try the controls. ───");
|
||||
window.Render();
|
||||
window.StartSync();
|
||||
|
||||
std::println("Done.");
|
||||
return 0;
|
||||
}
|
||||
23
examples/InputSystem/project.cpp
Normal file
23
examples/InputSystem/project.cpp
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
||||
Configuration* graphics = LocalProject({
|
||||
.projectFile = "../../project.cpp",
|
||||
.args = std::vector<std::string>(args.begin(), args.end()),
|
||||
});
|
||||
|
||||
Configuration cfg;
|
||||
cfg.path = "./";
|
||||
cfg.name = "InputSystem";
|
||||
cfg.outputName = "InputSystem";
|
||||
ApplyStandardArgs(cfg, args);
|
||||
cfg.dependencies = { graphics };
|
||||
|
||||
std::array<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "main" };
|
||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||
return cfg;
|
||||
}
|
||||
Binary file not shown.
|
|
@ -1,309 +0,0 @@
|
|||
// Spike for the OptionsMenu port (3DForts → Crafter.Graphics imperative UI).
|
||||
// Validates: Rect::SubRect carving at real-menu density, InputField shape and
|
||||
// caret blink, click-routing-to-focused-field, tab switching with per-tab
|
||||
// builders. Throwaway example, but kept around as a living reference.
|
||||
|
||||
#include "vulkan/vulkan.h"
|
||||
|
||||
import Crafter.Graphics;
|
||||
import Crafter.Event;
|
||||
import std;
|
||||
using namespace Crafter;
|
||||
|
||||
namespace {
|
||||
|
||||
enum class Tab : std::uint8_t { Graphics, Input, Audio };
|
||||
constexpr std::array<std::string_view, 3> kTabLabels = {
|
||||
"Graphics", "Input", "Audio"
|
||||
};
|
||||
|
||||
struct Layout {
|
||||
Rect canvas;
|
||||
Rect titleBar;
|
||||
Rect footer;
|
||||
Rect main;
|
||||
Rect tabStrip;
|
||||
Rect content;
|
||||
std::array<Rect, 3> tabRects;
|
||||
Rect btnExit;
|
||||
Rect btnSave;
|
||||
};
|
||||
|
||||
Layout ComputeLayout(const Window& window) {
|
||||
Layout L;
|
||||
L.canvas = Rect::FromWindow(window);
|
||||
L.titleBar = L.canvas.SubRect(60, Rect::Anchor::Top);
|
||||
L.footer = L.canvas.SubRect(60, Rect::Anchor::Bottom);
|
||||
L.main = L.canvas.Inset(60, 0, 60, 0);
|
||||
L.tabStrip = L.main.SubRect(220, Rect::Anchor::Left).Inset(20);
|
||||
L.content = L.main.Inset(0, 20, 0, 240);
|
||||
|
||||
Rect strip = L.tabStrip;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
L.tabRects[i] = strip.SubRect(48, Rect::Anchor::Top).Inset(0, 0, 8, 0);
|
||||
strip = strip.Inset(56, 0, 0, 0);
|
||||
}
|
||||
|
||||
Rect footerInset = L.footer.Inset(20, 60, 20, 60);
|
||||
L.btnExit = footerInset.SubRect(160, Rect::Anchor::Left);
|
||||
L.btnSave = footerInset.SubRect(160, Rect::Anchor::Right);
|
||||
return L;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
Device::Initialize();
|
||||
Window window(1280, 720, "OptionsSpike");
|
||||
|
||||
VkCommandBuffer init = window.StartInit();
|
||||
|
||||
DescriptorHeapVulkan heap;
|
||||
heap.Initialize(/*images*/ 16, /*buffers*/ 16, /*samplers*/ 4);
|
||||
window.descriptorHeap = &heap;
|
||||
|
||||
Font font("font.ttf");
|
||||
|
||||
FontAtlas atlas;
|
||||
atlas.Initialize(init);
|
||||
|
||||
UIRenderer ui;
|
||||
ui.fontAtlas = &atlas;
|
||||
ui.Initialize(window, heap, init);
|
||||
window.passes.push_back(&ui);
|
||||
|
||||
VulkanBuffer<QuadItem, true> quadsBuf;
|
||||
VulkanBuffer<GlyphItem, true> glyphsBuf;
|
||||
quadsBuf.Create(
|
||||
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
|
||||
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 256);
|
||||
glyphsBuf.Create(
|
||||
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
|
||||
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 4096);
|
||||
|
||||
auto quadsSlot = ui.RegisterBuffer(quadsBuf);
|
||||
auto glyphsSlot = ui.RegisterBuffer(glyphsBuf);
|
||||
|
||||
// Application state.
|
||||
Tab activeTab = Tab::Graphics;
|
||||
std::array<bool, 3> tabHover{};
|
||||
bool exitHover = false, saveHover = false;
|
||||
|
||||
// Graphics-tab fields. In the real port these are rebuilt per-tab; here we
|
||||
// just keep one set live and only show them on the Graphics tab to confirm
|
||||
// the routing pattern. Width/height come from a fake "tempOptions" state.
|
||||
InputField widthField { .value = "1920", .type = InputFieldType::UInt };
|
||||
InputField heightField { .value = "1080", .type = InputFieldType::UInt };
|
||||
std::array<InputField*, 2> fields{ &widthField, &heightField };
|
||||
std::array<Rect, 2> fieldRects{};
|
||||
std::array<bool, 2> fieldHover{};
|
||||
std::ptrdiff_t focusedField = -1;
|
||||
|
||||
auto BlurAll = [&]() {
|
||||
for (auto* f : fields) f->focused = false;
|
||||
focusedField = -1;
|
||||
};
|
||||
|
||||
auto SetFocus = [&](std::ptrdiff_t idx) {
|
||||
BlurAll();
|
||||
if (idx >= 0 && idx < std::ssize(fields)) {
|
||||
fields[idx]->focused = true;
|
||||
focusedField = idx;
|
||||
}
|
||||
};
|
||||
|
||||
auto LayoutGraphicsTabFields = [&](const Layout& L) {
|
||||
Rect rows = L.content;
|
||||
Rect row0 = rows.SubRect(48, Rect::Anchor::Top);
|
||||
rows = rows.Inset(56, 0, 0, 0);
|
||||
Rect row1 = rows.SubRect(48, Rect::Anchor::Top);
|
||||
|
||||
// Each row: label on left half, field on right half (right half inset).
|
||||
fieldRects[0] = row0.SubRect(row0.w * 0.5f, Rect::Anchor::Right).Inset(4);
|
||||
fieldRects[1] = row1.SubRect(row1.w * 0.5f, Rect::Anchor::Right).Inset(4);
|
||||
};
|
||||
|
||||
Layout L = ComputeLayout(window);
|
||||
LayoutGraphicsTabFields(L);
|
||||
|
||||
// ─── input listeners ───────────────────────────────────────────────
|
||||
EventListener<void> moveSub(&window.onMouseMove, [&]() {
|
||||
float mx = window.currentMousePos.x;
|
||||
float my = window.currentMousePos.y;
|
||||
for (int i = 0; i < 3; ++i) tabHover[i] = L.tabRects[i].Contains(mx, my);
|
||||
exitHover = L.btnExit.Contains(mx, my);
|
||||
saveHover = L.btnSave.Contains(mx, my);
|
||||
if (activeTab == Tab::Graphics) {
|
||||
for (int i = 0; i < (int)fields.size(); ++i)
|
||||
fieldHover[i] = fieldRects[i].Contains(mx, my);
|
||||
} else {
|
||||
for (auto& h : fieldHover) h = false;
|
||||
}
|
||||
});
|
||||
|
||||
EventListener<void> clickSub(&window.onMouseLeftClick, [&]() {
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (tabHover[i]) {
|
||||
activeTab = static_cast<Tab>(i);
|
||||
BlurAll();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (exitHover) { window.open = false; return; }
|
||||
if (saveHover) {
|
||||
// In the real port: OptionsIO::Save + Apply. Here we just print.
|
||||
std::println("[spike] save: width={} height={}",
|
||||
widthField.value, heightField.value);
|
||||
return;
|
||||
}
|
||||
if (activeTab == Tab::Graphics) {
|
||||
for (int i = 0; i < (int)fields.size(); ++i) {
|
||||
if (fieldHover[i]) {
|
||||
SetFocus(i);
|
||||
fields[i]->cursorPos = InputField_HitTestCursor(
|
||||
*fields[i], fieldRects[i],
|
||||
window.currentMousePos.x,
|
||||
font, 18.0f, InputFieldColors{
|
||||
.bg = {0.18f, 0.18f, 0.22f, 1.0f},
|
||||
.bgFocused = {0.22f, 0.22f, 0.30f, 1.0f},
|
||||
.border = {0.10f, 0.10f, 0.14f, 1.0f},
|
||||
.borderFocused = {0.40f, 0.65f, 1.00f, 1.0f},
|
||||
.text = {1.00f, 1.00f, 1.00f, 1.0f},
|
||||
.caret = {1.00f, 1.00f, 1.00f, 1.0f},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
BlurAll();
|
||||
});
|
||||
|
||||
EventListener<const std::string_view> textSub(&window.onTextInput, [&](std::string_view t) {
|
||||
if (focusedField >= 0) InputField_OnText(*fields[focusedField], t);
|
||||
});
|
||||
EventListener<CrafterKeys> keySub(&window.onAnyKeyDown, [&](CrafterKeys k) {
|
||||
if (focusedField >= 0) InputField_OnKey(*fields[focusedField], k);
|
||||
});
|
||||
|
||||
// ─── palettes ──────────────────────────────────────────────────────
|
||||
ButtonColors tabPalette{
|
||||
.bg = {0.20f, 0.22f, 0.28f, 1.0f},
|
||||
.bgHover = {0.30f, 0.55f, 0.95f, 1.0f},
|
||||
.bgPressed = {0.18f, 0.40f, 0.78f, 1.0f},
|
||||
.text = {1, 1, 1, 1},
|
||||
.cornerRadius = 6.0f,
|
||||
};
|
||||
ButtonColors tabPaletteSelected = tabPalette;
|
||||
tabPaletteSelected.bg = {0.30f, 0.55f, 0.95f, 1.0f};
|
||||
|
||||
ButtonColors footerPalette{
|
||||
.bg = {0.20f, 0.22f, 0.28f, 1.0f},
|
||||
.bgHover = {0.30f, 0.55f, 0.95f, 1.0f},
|
||||
.bgPressed = {0.18f, 0.40f, 0.78f, 1.0f},
|
||||
.text = {1, 1, 1, 1},
|
||||
.cornerRadius = 8.0f,
|
||||
};
|
||||
|
||||
InputFieldColors fieldPalette{
|
||||
.bg = {0.18f, 0.18f, 0.22f, 1.0f},
|
||||
.bgFocused = {0.22f, 0.22f, 0.30f, 1.0f},
|
||||
.border = {0.10f, 0.10f, 0.14f, 1.0f},
|
||||
.borderFocused = {0.40f, 0.65f, 1.00f, 1.0f},
|
||||
.text = {1, 1, 1, 1},
|
||||
.caret = {1, 1, 1, 1},
|
||||
};
|
||||
|
||||
auto startTime = std::chrono::steady_clock::now();
|
||||
|
||||
EventListener<UIBuildArgs> buildSub(&ui.onBuild, [&](UIBuildArgs a) {
|
||||
VkCommandBuffer cmd = a.cmd;
|
||||
|
||||
L = ComputeLayout(window);
|
||||
LayoutGraphicsTabFields(L);
|
||||
|
||||
std::uint32_t qc = 0, gc = 0;
|
||||
UIBuffer buf{
|
||||
.quads = quadsBuf.value,
|
||||
.quadCount = &qc,
|
||||
.quadCap = 256,
|
||||
.glyphs = glyphsBuf.value,
|
||||
.glyphCount = &gc,
|
||||
.glyphCap = 4096,
|
||||
.atlas = &atlas,
|
||||
.renderer = &ui,
|
||||
};
|
||||
|
||||
// Background.
|
||||
if (qc < buf.quadCap) buf.quads[qc++] = QuadItem{
|
||||
L.canvas.x, L.canvas.y, L.canvas.w, L.canvas.h,
|
||||
0.10f, 0.10f, 0.13f, 1.0f,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
};
|
||||
// Title bar.
|
||||
if (qc < buf.quadCap) buf.quads[qc++] = QuadItem{
|
||||
L.titleBar.x, L.titleBar.y, L.titleBar.w, L.titleBar.h,
|
||||
0.14f, 0.14f, 0.18f, 1.0f,
|
||||
0, 0, 0, 0, 0, 0, 0, 0,
|
||||
};
|
||||
DrawText(buf, "OPTIONS",
|
||||
L.titleBar.x + L.titleBar.w * 0.5f,
|
||||
L.titleBar.y + L.titleBar.h * 0.5f + 18.0f * 0.32f,
|
||||
font, 24.0f, {1, 1, 1, 1}, TextAlign::Center);
|
||||
|
||||
// Tabs.
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const auto& palette = (activeTab == static_cast<Tab>(i))
|
||||
? tabPaletteSelected : tabPalette;
|
||||
DrawButton(buf, L.tabRects[i], kTabLabels[i],
|
||||
tabHover[i], false, font, 18.0f, palette);
|
||||
}
|
||||
|
||||
// Active tab content.
|
||||
if (activeTab == Tab::Graphics) {
|
||||
Rect rows = L.content;
|
||||
Rect row0 = rows.SubRect(48, Rect::Anchor::Top);
|
||||
rows = rows.Inset(56, 0, 0, 0);
|
||||
Rect row1 = rows.SubRect(48, Rect::Anchor::Top);
|
||||
|
||||
DrawText(buf, "Resolution width",
|
||||
row0.x + 8.0f,
|
||||
row0.y + row0.h * 0.5f + 18.0f * 0.32f,
|
||||
font, 18.0f, {0.85f, 0.85f, 0.85f, 1.0f}, TextAlign::Left);
|
||||
DrawText(buf, "Resolution height",
|
||||
row1.x + 8.0f,
|
||||
row1.y + row1.h * 0.5f + 18.0f * 0.32f,
|
||||
font, 18.0f, {0.85f, 0.85f, 0.85f, 1.0f}, TextAlign::Left);
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now - startTime).count();
|
||||
bool caretVisible = ((ms / 500) & 1) == 0;
|
||||
|
||||
DrawInputField(buf, widthField, fieldRects[0], font, 18.0f, fieldPalette, caretVisible);
|
||||
DrawInputField(buf, heightField, fieldRects[1], font, 18.0f, fieldPalette, caretVisible);
|
||||
} else {
|
||||
DrawText(buf, "(tab content not populated in spike)",
|
||||
L.content.x + L.content.w * 0.5f,
|
||||
L.content.y + L.content.h * 0.5f,
|
||||
font, 18.0f, {0.6f, 0.6f, 0.6f, 1.0f}, TextAlign::Center);
|
||||
}
|
||||
|
||||
// Footer.
|
||||
DrawButton(buf, L.btnExit, "Exit", exitHover, false, font, 18.0f, footerPalette);
|
||||
DrawButton(buf, L.btnSave, "Save changes", saveHover, false, font, 18.0f, footerPalette);
|
||||
|
||||
// Dispatch.
|
||||
if (qc > 0) {
|
||||
quadsBuf.FlushDevice(cmd, VK_ACCESS_SHADER_READ_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
|
||||
ui.DispatchQuads(cmd, quadsSlot, qc);
|
||||
}
|
||||
if (gc > 0) {
|
||||
glyphsBuf.FlushDevice(cmd, VK_ACCESS_SHADER_READ_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
|
||||
ui.DispatchText(cmd, glyphsSlot, gc);
|
||||
}
|
||||
});
|
||||
|
||||
window.FinishInit();
|
||||
window.Render();
|
||||
window.StartUpdate();
|
||||
window.StartSync();
|
||||
}
|
||||
|
|
@ -43,6 +43,27 @@ library does not track widgets or focus.
|
|||
Drop a TTF in this directory as `font.ttf` before running (the example
|
||||
loads it via `Font("font.ttf")`).
|
||||
|
||||
### [InputSystem](InputSystem/)
|
||||
Guided tour of `Crafter::Input`: name actions ("Jump", "Move", "Fire",
|
||||
"Look", "Zoom"), bind them to keys / mouse / gamepad (with composite
|
||||
bindings for WASD-as-Vector2 and analog sticks), and consume them as
|
||||
events. Demonstrates:
|
||||
|
||||
- The compile-time `Key(CrafterKeys::Space)` helper that folds to a
|
||||
per-platform raw scancode — bindings stay cross-platform-readable
|
||||
in source while runtime data stores raw codes only.
|
||||
- All four action types (Button, Axis1D, Vector2) with multiple
|
||||
bindings per action (any-of semantics).
|
||||
- `Map::StartRebind` — press R, then press any input to remap "Jump"
|
||||
at runtime. Captured input is filtered out for that frame.
|
||||
- `BindingToString` / `BindingFromString` round-trip — print the
|
||||
default bindings as the on-disk format.
|
||||
- Gamepad hot-plug events: plug a controller in mid-run and the
|
||||
bindings start firing immediately.
|
||||
|
||||
Console-driven (no UI rendering needed); focus the window and watch
|
||||
stdout.
|
||||
|
||||
### [CustomShader](CustomShader/)
|
||||
Tier 1 demo: a user-authored compute shader (`inverse-circle.comp.glsl`)
|
||||
running alongside the shipped `drawQuads`. The custom shader inverts RGB
|
||||
|
|
|
|||
174
implementations/Crafter.Graphics-Clipboard.cpp
Normal file
174
implementations/Crafter.Graphics-Clipboard.cpp
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
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 <wayland-client.h>
|
||||
#include <wayland-client-protocol.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#endif
|
||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMINMAX
|
||||
#include <windows.h>
|
||||
#endif
|
||||
module Crafter.Graphics;
|
||||
import std;
|
||||
|
||||
using namespace Crafter;
|
||||
|
||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
||||
namespace {
|
||||
// Heap-allocated state attached to each wl_data_source via its
|
||||
// user_data slot. The text must outlive the source (the compositor
|
||||
// can call `send` minutes after we set the selection, every time a
|
||||
// remote app pastes), so we own it here and free it from
|
||||
// `OnSourceCancelled` — which is the only callback the compositor
|
||||
// guarantees will fire when the source is no longer needed (when
|
||||
// the selection is replaced, or on app exit).
|
||||
struct Held {
|
||||
std::string text;
|
||||
};
|
||||
|
||||
void OnSourceTarget(void*, wl_data_source*, const char*) {}
|
||||
|
||||
void OnSourceSend(void* data, wl_data_source*,
|
||||
const char* /*mime_type*/, std::int32_t fd) {
|
||||
// We only ever advertise text MIME types, so any negotiated
|
||||
// type maps to the same UTF-8 buffer. `send` may fire multiple
|
||||
// times across the lifetime of one selection (each paste is a
|
||||
// fresh fd), so we must not consume `text` here.
|
||||
Held* h = static_cast<Held*>(data);
|
||||
const char* p = h->text.data();
|
||||
std::size_t rem = h->text.size();
|
||||
while (rem > 0) {
|
||||
const ssize_t w = ::write(fd, p, rem);
|
||||
if (w < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
break; // pipe closed; remote gave up reading
|
||||
}
|
||||
if (w == 0) break;
|
||||
p += w;
|
||||
rem -= static_cast<std::size_t>(w);
|
||||
}
|
||||
::close(fd);
|
||||
}
|
||||
|
||||
void OnSourceCancelled(void* data, wl_data_source* source) {
|
||||
// Selection was replaced (someone else copied) or the app is
|
||||
// exiting. Either way, this is our hook to release the held
|
||||
// text and the source itself.
|
||||
delete static_cast<Held*>(data);
|
||||
wl_data_source_destroy(source);
|
||||
}
|
||||
|
||||
// Drag-and-drop only callbacks. We never start a DnD action, so
|
||||
// these can't fire — but the listener struct's size is fixed at
|
||||
// the v3 shape, and wayland-client uses the struct's slots
|
||||
// directly. Stubs are required.
|
||||
void OnSourceDndDropPerformed(void*, wl_data_source*) {}
|
||||
void OnSourceDndFinished(void*, wl_data_source*) {}
|
||||
void OnSourceAction(void*, wl_data_source*, std::uint32_t) {}
|
||||
|
||||
constexpr wl_data_source_listener kSourceListener = {
|
||||
.target = OnSourceTarget,
|
||||
.send = OnSourceSend,
|
||||
.cancelled = OnSourceCancelled,
|
||||
.dnd_drop_performed = OnSourceDndDropPerformed,
|
||||
.dnd_finished = OnSourceDndFinished,
|
||||
.action = OnSourceAction,
|
||||
};
|
||||
}
|
||||
#endif
|
||||
|
||||
bool Crafter::Clipboard::SetText(std::string_view text) {
|
||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
||||
if (Device::dataDeviceManager == nullptr || Device::dataDevice == nullptr) {
|
||||
// Compositor doesn't expose wl_data_device_manager (rare; some
|
||||
// headless / minimal compositors). Caller can fall back.
|
||||
return false;
|
||||
}
|
||||
// wl_data_device.set_selection requires a serial from a recent
|
||||
// input event. We track the most recent pointer-enter serial on
|
||||
// each window — fine for "user clicked Copy" interactions, since
|
||||
// the click itself produced the serial. Without one, the
|
||||
// compositor would silently reject the request.
|
||||
std::uint32_t serial = 0;
|
||||
if (Device::focusedWindow != nullptr) {
|
||||
serial = Device::focusedWindow->lastPointerSerial_;
|
||||
}
|
||||
if (serial == 0) return false;
|
||||
|
||||
auto* held = new Held{ std::string(text) };
|
||||
wl_data_source* source =
|
||||
wl_data_device_manager_create_data_source(Device::dataDeviceManager);
|
||||
if (source == nullptr) {
|
||||
delete held;
|
||||
return false;
|
||||
}
|
||||
wl_data_source_add_listener(source, &kSourceListener, held);
|
||||
|
||||
// Advertise the four common text MIME types so legacy + modern
|
||||
// pasters both find a match. Wayland clients prefer the first one
|
||||
// they recognise, in order.
|
||||
wl_data_source_offer(source, "text/plain;charset=utf-8");
|
||||
wl_data_source_offer(source, "text/plain");
|
||||
wl_data_source_offer(source, "UTF8_STRING");
|
||||
wl_data_source_offer(source, "TEXT");
|
||||
|
||||
wl_data_device_set_selection(Device::dataDevice, source, serial);
|
||||
// Push the request so the compositor sees it before the next event
|
||||
// loop iteration — otherwise a quick read in another app might
|
||||
// miss the selection update.
|
||||
wl_display_flush(Device::display);
|
||||
return true;
|
||||
#elif defined(CRAFTER_GRAPHICS_WINDOW_WIN32)
|
||||
// CF_UNICODETEXT round-trip. Convert UTF-8 → UTF-16, allocate a
|
||||
// moveable HGLOBAL, hand it off to the OS. The OS frees the global
|
||||
// when the next clipboard owner replaces the data, so our caller
|
||||
// doesn't need to keep `text` alive — same lifetime contract as
|
||||
// the Wayland path.
|
||||
if (!OpenClipboard(nullptr)) return false;
|
||||
EmptyClipboard();
|
||||
const int wlen = MultiByteToWideChar(CP_UTF8, 0,
|
||||
text.data(), static_cast<int>(text.size()), nullptr, 0);
|
||||
if (wlen < 0) { CloseClipboard(); return false; }
|
||||
HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE,
|
||||
(static_cast<std::size_t>(wlen) + 1) * sizeof(wchar_t));
|
||||
if (h == nullptr) { CloseClipboard(); return false; }
|
||||
wchar_t* dst = static_cast<wchar_t*>(GlobalLock(h));
|
||||
if (dst == nullptr) {
|
||||
GlobalFree(h);
|
||||
CloseClipboard();
|
||||
return false;
|
||||
}
|
||||
MultiByteToWideChar(CP_UTF8, 0,
|
||||
text.data(), static_cast<int>(text.size()), dst, wlen);
|
||||
dst[wlen] = L'\0';
|
||||
GlobalUnlock(h);
|
||||
const bool ok = SetClipboardData(CF_UNICODETEXT, h) != nullptr;
|
||||
if (!ok) GlobalFree(h); // OS only takes ownership on success
|
||||
CloseClipboard();
|
||||
return ok;
|
||||
#else
|
||||
(void)text;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
|
@ -176,158 +176,6 @@ VkBool32 onError(VkDebugUtilsMessageSeverityFlagBitsEXT severity, VkDebugUtilsMe
|
|||
|
||||
|
||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
||||
constexpr CrafterKeys keysym_to_crafter_key(xkb_keysym_t sym)
|
||||
{
|
||||
switch (sym)
|
||||
{
|
||||
// Alphabet
|
||||
case XKB_KEY_a: return CrafterKeys::A;
|
||||
case XKB_KEY_b: return CrafterKeys::B;
|
||||
case XKB_KEY_c: return CrafterKeys::C;
|
||||
case XKB_KEY_d: return CrafterKeys::D;
|
||||
case XKB_KEY_e: return CrafterKeys::E;
|
||||
case XKB_KEY_f: return CrafterKeys::F;
|
||||
case XKB_KEY_g: return CrafterKeys::G;
|
||||
case XKB_KEY_h: return CrafterKeys::H;
|
||||
case XKB_KEY_i: return CrafterKeys::I;
|
||||
case XKB_KEY_j: return CrafterKeys::J;
|
||||
case XKB_KEY_k: return CrafterKeys::K;
|
||||
case XKB_KEY_l: return CrafterKeys::L;
|
||||
case XKB_KEY_m: return CrafterKeys::M;
|
||||
case XKB_KEY_n: return CrafterKeys::N;
|
||||
case XKB_KEY_o: return CrafterKeys::O;
|
||||
case XKB_KEY_p: return CrafterKeys::P;
|
||||
case XKB_KEY_q: return CrafterKeys::Q;
|
||||
case XKB_KEY_r: return CrafterKeys::R;
|
||||
case XKB_KEY_s: return CrafterKeys::S;
|
||||
case XKB_KEY_t: return CrafterKeys::T;
|
||||
case XKB_KEY_u: return CrafterKeys::U;
|
||||
case XKB_KEY_v: return CrafterKeys::V;
|
||||
case XKB_KEY_w: return CrafterKeys::W;
|
||||
case XKB_KEY_x: return CrafterKeys::X;
|
||||
case XKB_KEY_y: return CrafterKeys::Y;
|
||||
case XKB_KEY_z: return CrafterKeys::Z;
|
||||
|
||||
case XKB_KEY_A: return CrafterKeys::A;
|
||||
case XKB_KEY_B: return CrafterKeys::B;
|
||||
case XKB_KEY_C: return CrafterKeys::C;
|
||||
case XKB_KEY_D: return CrafterKeys::D;
|
||||
case XKB_KEY_E: return CrafterKeys::E;
|
||||
case XKB_KEY_F: return CrafterKeys::F;
|
||||
case XKB_KEY_G: return CrafterKeys::G;
|
||||
case XKB_KEY_H: return CrafterKeys::H;
|
||||
case XKB_KEY_I: return CrafterKeys::I;
|
||||
case XKB_KEY_J: return CrafterKeys::J;
|
||||
case XKB_KEY_K: return CrafterKeys::K;
|
||||
case XKB_KEY_L: return CrafterKeys::L;
|
||||
case XKB_KEY_M: return CrafterKeys::M;
|
||||
case XKB_KEY_N: return CrafterKeys::N;
|
||||
case XKB_KEY_O: return CrafterKeys::O;
|
||||
case XKB_KEY_P: return CrafterKeys::P;
|
||||
case XKB_KEY_Q: return CrafterKeys::Q;
|
||||
case XKB_KEY_R: return CrafterKeys::R;
|
||||
case XKB_KEY_S: return CrafterKeys::S;
|
||||
case XKB_KEY_T: return CrafterKeys::T;
|
||||
case XKB_KEY_U: return CrafterKeys::U;
|
||||
case XKB_KEY_V: return CrafterKeys::V;
|
||||
case XKB_KEY_W: return CrafterKeys::W;
|
||||
case XKB_KEY_X: return CrafterKeys::X;
|
||||
case XKB_KEY_Y: return CrafterKeys::Y;
|
||||
case XKB_KEY_Z: return CrafterKeys::Z;
|
||||
|
||||
// Numbers
|
||||
case XKB_KEY_0: return CrafterKeys::_0;
|
||||
case XKB_KEY_1: return CrafterKeys::_1;
|
||||
case XKB_KEY_2: return CrafterKeys::_2;
|
||||
case XKB_KEY_3: return CrafterKeys::_3;
|
||||
case XKB_KEY_4: return CrafterKeys::_4;
|
||||
case XKB_KEY_5: return CrafterKeys::_5;
|
||||
case XKB_KEY_6: return CrafterKeys::_6;
|
||||
case XKB_KEY_7: return CrafterKeys::_7;
|
||||
case XKB_KEY_8: return CrafterKeys::_8;
|
||||
case XKB_KEY_9: return CrafterKeys::_9;
|
||||
|
||||
// Function keys
|
||||
case XKB_KEY_F1: return CrafterKeys::F1;
|
||||
case XKB_KEY_F2: return CrafterKeys::F2;
|
||||
case XKB_KEY_F3: return CrafterKeys::F3;
|
||||
case XKB_KEY_F4: return CrafterKeys::F4;
|
||||
case XKB_KEY_F5: return CrafterKeys::F5;
|
||||
case XKB_KEY_F6: return CrafterKeys::F6;
|
||||
case XKB_KEY_F7: return CrafterKeys::F7;
|
||||
case XKB_KEY_F8: return CrafterKeys::F8;
|
||||
case XKB_KEY_F9: return CrafterKeys::F9;
|
||||
case XKB_KEY_F10: return CrafterKeys::F10;
|
||||
case XKB_KEY_F11: return CrafterKeys::F11;
|
||||
case XKB_KEY_F12: return CrafterKeys::F12;
|
||||
|
||||
// Control keys
|
||||
case XKB_KEY_Escape: return CrafterKeys::Escape;
|
||||
case XKB_KEY_Tab: return CrafterKeys::Tab;
|
||||
case XKB_KEY_Return: return CrafterKeys::Enter;
|
||||
case XKB_KEY_space: return CrafterKeys::Space;
|
||||
case XKB_KEY_BackSpace: return CrafterKeys::Backspace;
|
||||
case XKB_KEY_Delete: return CrafterKeys::Delete;
|
||||
case XKB_KEY_Insert: return CrafterKeys::Insert;
|
||||
case XKB_KEY_Home: return CrafterKeys::Home;
|
||||
case XKB_KEY_End: return CrafterKeys::End;
|
||||
case XKB_KEY_Page_Up: return CrafterKeys::PageUp;
|
||||
case XKB_KEY_Page_Down: return CrafterKeys::PageDown;
|
||||
case XKB_KEY_Caps_Lock: return CrafterKeys::CapsLock;
|
||||
case XKB_KEY_Num_Lock: return CrafterKeys::NumLock;
|
||||
case XKB_KEY_Scroll_Lock:return CrafterKeys::ScrollLock;
|
||||
|
||||
// Modifiers
|
||||
case XKB_KEY_Shift_L: return CrafterKeys::LeftShift;
|
||||
case XKB_KEY_Shift_R: return CrafterKeys::RightShift;
|
||||
case XKB_KEY_Control_L: return CrafterKeys::LeftCtrl;
|
||||
case XKB_KEY_Control_R: return CrafterKeys::RightCtrl;
|
||||
case XKB_KEY_Alt_L: return CrafterKeys::LeftAlt;
|
||||
case XKB_KEY_Alt_R: return CrafterKeys::RightAlt;
|
||||
case XKB_KEY_Super_L: return CrafterKeys::LeftSuper;
|
||||
case XKB_KEY_Super_R: return CrafterKeys::RightSuper;
|
||||
|
||||
// Arrows
|
||||
case XKB_KEY_Up: return CrafterKeys::Up;
|
||||
case XKB_KEY_Down: return CrafterKeys::Down;
|
||||
case XKB_KEY_Left: return CrafterKeys::Left;
|
||||
case XKB_KEY_Right: return CrafterKeys::Right;
|
||||
|
||||
// Keypad
|
||||
case XKB_KEY_KP_0: return CrafterKeys::keypad_0;
|
||||
case XKB_KEY_KP_1: return CrafterKeys::keypad_1;
|
||||
case XKB_KEY_KP_2: return CrafterKeys::keypad_2;
|
||||
case XKB_KEY_KP_3: return CrafterKeys::keypad_3;
|
||||
case XKB_KEY_KP_4: return CrafterKeys::keypad_4;
|
||||
case XKB_KEY_KP_5: return CrafterKeys::keypad_5;
|
||||
case XKB_KEY_KP_6: return CrafterKeys::keypad_6;
|
||||
case XKB_KEY_KP_7: return CrafterKeys::keypad_7;
|
||||
case XKB_KEY_KP_8: return CrafterKeys::keypad_8;
|
||||
case XKB_KEY_KP_9: return CrafterKeys::keypad_9;
|
||||
case XKB_KEY_KP_Enter: return CrafterKeys::keypad_enter;
|
||||
case XKB_KEY_KP_Add: return CrafterKeys::keypad_plus;
|
||||
case XKB_KEY_KP_Subtract: return CrafterKeys::keypad_minus;
|
||||
case XKB_KEY_KP_Multiply: return CrafterKeys::keypad_multiply;
|
||||
case XKB_KEY_KP_Divide: return CrafterKeys::keypad_divide;
|
||||
case XKB_KEY_KP_Decimal: return CrafterKeys::keypad_decimal;
|
||||
|
||||
// Punctuation
|
||||
case XKB_KEY_grave: return CrafterKeys::grave;
|
||||
case XKB_KEY_minus: return CrafterKeys::minus;
|
||||
case XKB_KEY_equal: return CrafterKeys::equal;
|
||||
case XKB_KEY_bracketleft: return CrafterKeys::bracket_left;
|
||||
case XKB_KEY_bracketright:return CrafterKeys::bracket_right;
|
||||
case XKB_KEY_backslash: return CrafterKeys::backslash;
|
||||
case XKB_KEY_semicolon: return CrafterKeys::semicolon;
|
||||
case XKB_KEY_apostrophe: return CrafterKeys::quote;
|
||||
case XKB_KEY_comma: return CrafterKeys::comma;
|
||||
case XKB_KEY_period: return CrafterKeys::period;
|
||||
case XKB_KEY_slash: return CrafterKeys::slash;
|
||||
|
||||
default: return CrafterKeys::CrafterKeysMax;
|
||||
}
|
||||
}
|
||||
|
||||
void Device::xdg_wm_base_handle_ping(void* data, xdg_wm_base* xdg_wm_base, std::uint32_t serial) {
|
||||
xdg_wm_base_pong(xdg_wm_base, serial);
|
||||
}
|
||||
|
|
@ -336,8 +184,15 @@ void Device::handle_global(void *data, wl_registry *registry, std::uint32_t name
|
|||
if (strcmp(interface, wl_shm_interface.name) == 0) {
|
||||
shm = reinterpret_cast<wl_shm*>(wl_registry_bind(registry, name, &wl_shm_interface, 1));
|
||||
} else if (strcmp(interface, wl_seat_interface.name) == 0) {
|
||||
wl_seat* seat = reinterpret_cast<wl_seat*>(wl_registry_bind(registry, name, &wl_seat_interface, 1));
|
||||
// Assign to Device::seat (not a fresh local) so SetClipboardText
|
||||
// and any other code that needs the seat post-init can find it.
|
||||
seat = reinterpret_cast<wl_seat*>(wl_registry_bind(registry, name, &wl_seat_interface, 1));
|
||||
wl_seat_add_listener(seat, &seat_listener, nullptr);
|
||||
// If the manager came in first, the data device couldn't be
|
||||
// created yet — do it now that we have the seat.
|
||||
if (dataDeviceManager != nullptr && dataDevice == nullptr) {
|
||||
dataDevice = wl_data_device_manager_get_data_device(dataDeviceManager, seat);
|
||||
}
|
||||
} else if (compositor == nullptr && strcmp(interface, wl_compositor_interface.name) == 0) {
|
||||
compositor = reinterpret_cast<wl_compositor*>(wl_registry_bind(registry, name, &wl_compositor_interface, 3));
|
||||
} else if (strcmp(interface, xdg_wm_base_interface.name) == 0) {
|
||||
|
|
@ -349,6 +204,16 @@ void Device::handle_global(void *data, wl_registry *registry, std::uint32_t name
|
|||
wpViewporter = reinterpret_cast<wp_viewporter*>(wl_registry_bind(registry, name, &wp_viewporter_interface, 1));
|
||||
} else if (strcmp(interface, wp_fractional_scale_manager_v1_interface.name) == 0) {
|
||||
fractionalScaleManager = reinterpret_cast<wp_fractional_scale_manager_v1*>(wl_registry_bind(registry, name, &wp_fractional_scale_manager_v1_interface, 1));
|
||||
} else if (strcmp(interface, wl_data_device_manager_interface.name) == 0) {
|
||||
// v3 gives us the full set of data_source events (target / send /
|
||||
// cancelled / dnd_*). Universally supported by the compositors
|
||||
// we target — fall back path is the per-source listener simply
|
||||
// not getting the v3-only callbacks.
|
||||
dataDeviceManager = reinterpret_cast<wl_data_device_manager*>(
|
||||
wl_registry_bind(registry, name, &wl_data_device_manager_interface, 3));
|
||||
if (seat != nullptr && dataDevice == nullptr) {
|
||||
dataDevice = wl_data_device_manager_get_data_device(dataDeviceManager, seat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -441,23 +306,24 @@ void Device::keyboard_leave(void *data, wl_keyboard *keyboard, uint32_t serial,
|
|||
}
|
||||
|
||||
void Device::keyboard_key(void *data, wl_keyboard *keyboard, uint32_t serial, uint32_t time, uint32_t key, uint32_t state) {
|
||||
xkb_keycode_t keycode = key + 8;
|
||||
xkb_keysym_t keysym = xkb_state_key_get_one_sym(xkb_state, keycode);
|
||||
CrafterKeys crafterKey = keysym_to_crafter_key(keysym);
|
||||
// `key` is the kernel input-event-code (KEY_*). That is exactly what
|
||||
// :Keys returns for Wayland builds, so we store it verbatim with no
|
||||
// translation. The +8 X11 offset is only needed for the XKB layer,
|
||||
// which we still consult to produce UTF-8 text.
|
||||
KeyCode code = key;
|
||||
xkb_keycode_t xkbKeycode = key + 8;
|
||||
|
||||
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
||||
if (focusedWindow->heldkeys[(std::uint8_t)crafterKey]) {
|
||||
focusedWindow->onKeyHold[(std::uint8_t)crafterKey].Invoke();
|
||||
focusedWindow->onAnyKeyHold.Invoke(crafterKey);
|
||||
if (focusedWindow->heldKeys.contains(code)) {
|
||||
focusedWindow->onRawKeyHold.Invoke(code);
|
||||
} else {
|
||||
focusedWindow->heldkeys[(std::uint8_t)crafterKey] = true;
|
||||
focusedWindow->onKeyDown[(std::uint8_t)crafterKey].Invoke();
|
||||
focusedWindow->onAnyKeyDown.Invoke(crafterKey);
|
||||
focusedWindow->heldKeys.insert(code);
|
||||
focusedWindow->onRawKeyDown.Invoke(code);
|
||||
}
|
||||
|
||||
std::string buf;
|
||||
buf.resize(16);
|
||||
int n = xkb_state_key_get_utf8(xkb_state, keycode, buf.data(), 16);
|
||||
int n = xkb_state_key_get_utf8(xkb_state, xkbKeycode, buf.data(), 16);
|
||||
std::string utf8;
|
||||
if (n > 0 && (unsigned char)buf[0] >= 0x20 && buf[0] != 0x7f) {
|
||||
buf.resize(n);
|
||||
|
|
@ -468,19 +334,18 @@ void Device::keyboard_key(void *data, wl_keyboard *keyboard, uint32_t serial, ui
|
|||
// Replace the active repeat with this key — most recent press wins,
|
||||
// matching xkbcommon's typical behaviour and most desktop apps.
|
||||
keyRepeat.active = (keyRepeat.rate > 0);
|
||||
keyRepeat.key = crafterKey;
|
||||
keyRepeat.key = code;
|
||||
keyRepeat.utf8 = std::move(utf8);
|
||||
keyRepeat.pressTime = std::chrono::steady_clock::now();
|
||||
keyRepeat.lastFireTime = keyRepeat.pressTime;
|
||||
} else {
|
||||
focusedWindow->heldkeys[(std::uint8_t)crafterKey] = false;
|
||||
focusedWindow->onKeyUp[(std::uint8_t)crafterKey].Invoke();
|
||||
focusedWindow->onAnyKeyUp.Invoke(crafterKey);
|
||||
focusedWindow->heldKeys.erase(code);
|
||||
focusedWindow->onRawKeyUp.Invoke(code);
|
||||
|
||||
// If the released key was the one repeating, stop. Otherwise leave
|
||||
// the existing repeat alone (user pressed/released a modifier
|
||||
// mid-repeat etc.).
|
||||
if (keyRepeat.active && keyRepeat.key == crafterKey) {
|
||||
if (keyRepeat.active && keyRepeat.key == code) {
|
||||
keyRepeat.active = false;
|
||||
keyRepeat.utf8.clear();
|
||||
}
|
||||
|
|
@ -513,10 +378,8 @@ void Device::TickKeyRepeats() {
|
|||
// Catch up — emit one event per missed period so a paused frame doesn't
|
||||
// make the repeat permanently lag behind.
|
||||
while (now - keyRepeat.lastFireTime >= period) {
|
||||
focusedWindow->onKeyDown[(std::uint8_t)keyRepeat.key].Invoke();
|
||||
focusedWindow->onAnyKeyDown.Invoke(keyRepeat.key);
|
||||
focusedWindow->onKeyHold[(std::uint8_t)keyRepeat.key].Invoke();
|
||||
focusedWindow->onAnyKeyHold.Invoke(keyRepeat.key);
|
||||
focusedWindow->onRawKeyDown.Invoke(keyRepeat.key);
|
||||
focusedWindow->onRawKeyHold.Invoke(keyRepeat.key);
|
||||
if (!keyRepeat.utf8.empty()) {
|
||||
focusedWindow->onTextInput.Invoke(keyRepeat.utf8);
|
||||
}
|
||||
|
|
@ -673,12 +536,41 @@ void Device::Initialize() {
|
|||
}
|
||||
}
|
||||
|
||||
// Enumerate available device extensions so we can opt into
|
||||
// VK_EXT_memory_decompression when the driver advertises it. Drivers
|
||||
// without it (AMD, Intel as of early 2026) get the CPU-decode fallback.
|
||||
{
|
||||
std::uint32_t extCount = 0;
|
||||
vkEnumerateDeviceExtensionProperties(physDevice, nullptr, &extCount, nullptr);
|
||||
std::vector<VkExtensionProperties> exts(extCount);
|
||||
vkEnumerateDeviceExtensionProperties(physDevice, nullptr, &extCount, exts.data());
|
||||
for (const VkExtensionProperties& e : exts) {
|
||||
if (std::strcmp(e.extensionName, VK_EXT_MEMORY_DECOMPRESSION_EXTENSION_NAME) == 0) {
|
||||
memoryDecompressionSupported = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Properties query: chain memory-decompression props only when supported,
|
||||
// otherwise sType validation flags it as an unrecognized struct on
|
||||
// drivers that don't expose the extension.
|
||||
if (memoryDecompressionSupported) {
|
||||
memoryDecompressionProperties.pNext = const_cast<void*>(rayTracingProperties.pNext);
|
||||
rayTracingProperties.pNext = &memoryDecompressionProperties;
|
||||
}
|
||||
VkPhysicalDeviceProperties2 properties2 {
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2,
|
||||
.pNext = &rayTracingProperties
|
||||
};
|
||||
vkGetPhysicalDeviceProperties2(physDevice, &properties2);
|
||||
|
||||
// Sanity-gate: GDeflate 1.0 must actually be in the supported method set.
|
||||
if (memoryDecompressionSupported &&
|
||||
(memoryDecompressionProperties.decompressionMethods & VK_MEMORY_DECOMPRESSION_METHOD_GDEFLATE_1_0_BIT_EXT) == 0) {
|
||||
memoryDecompressionSupported = false;
|
||||
}
|
||||
|
||||
uint32_t queueFamilyCount;
|
||||
vkGetPhysicalDeviceQueueFamilyProperties(physDevice, &queueFamilyCount, NULL);
|
||||
|
||||
|
|
@ -724,9 +616,26 @@ void Device::Initialize() {
|
|||
.shaderUntypedPointers = VK_TRUE,
|
||||
};
|
||||
|
||||
// Enables synchronization2 sentinels (VkMemoryBarrier2, VK_PIPELINE_STAGE_2_*,
|
||||
// VK_ACCESS_2_*) — required for VK_EXT_memory_decompression's sync tokens.
|
||||
VkPhysicalDeviceVulkan13Features features13 {
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES,
|
||||
.pNext = &untypedPointersFeatures,
|
||||
.synchronization2 = VK_TRUE,
|
||||
};
|
||||
|
||||
VkPhysicalDeviceMemoryDecompressionFeaturesEXT memoryDecompressionFeatures {
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_DECOMPRESSION_FEATURES_EXT,
|
||||
.pNext = &features13,
|
||||
.memoryDecompression = VK_TRUE,
|
||||
};
|
||||
void* postDecompressChain = memoryDecompressionSupported
|
||||
? static_cast<void*>(&memoryDecompressionFeatures)
|
||||
: static_cast<void*>(&features13);
|
||||
|
||||
VkPhysicalDeviceDescriptorHeapFeaturesEXT desciptorHeapFeatures {
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_HEAP_FEATURES_EXT,
|
||||
.pNext = &untypedPointersFeatures,
|
||||
.pNext = postDecompressChain,
|
||||
.descriptorHeap = VK_TRUE,
|
||||
};
|
||||
|
||||
|
|
@ -787,12 +696,22 @@ void Device::Initialize() {
|
|||
}
|
||||
};
|
||||
|
||||
// Build the enabled-extension list dynamically so we can append the
|
||||
// optional VK_EXT_memory_decompression entry only when the driver
|
||||
// advertises it.
|
||||
std::vector<const char*> enabledDeviceExtensions(
|
||||
std::begin(deviceExtensionNames),
|
||||
std::end(deviceExtensionNames));
|
||||
if (memoryDecompressionSupported) {
|
||||
enabledDeviceExtensions.push_back(VK_EXT_MEMORY_DECOMPRESSION_EXTENSION_NAME);
|
||||
}
|
||||
|
||||
VkDeviceCreateInfo deviceCreateInfo = {};
|
||||
deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
|
||||
deviceCreateInfo.queueCreateInfoCount = 1;
|
||||
deviceCreateInfo.pQueueCreateInfos = &queueCreateInfo;
|
||||
deviceCreateInfo.enabledExtensionCount = sizeof(deviceExtensionNames) / sizeof(const char*);
|
||||
deviceCreateInfo.ppEnabledExtensionNames = deviceExtensionNames;
|
||||
deviceCreateInfo.enabledExtensionCount = static_cast<std::uint32_t>(enabledDeviceExtensions.size());
|
||||
deviceCreateInfo.ppEnabledExtensionNames = enabledDeviceExtensions.data();
|
||||
deviceCreateInfo.pNext = &physical_features2;
|
||||
|
||||
uint32_t deviceLayerCount;
|
||||
|
|
@ -846,6 +765,19 @@ void Device::Initialize() {
|
|||
vkCmdPushDataEXT = reinterpret_cast<PFN_vkCmdPushDataEXT>(vkGetInstanceProcAddr(instance, "vkCmdPushDataEXT"));
|
||||
vkGetPhysicalDeviceDescriptorSizeEXT = reinterpret_cast<PFN_vkGetPhysicalDeviceDescriptorSizeEXT>(vkGetInstanceProcAddr(instance, "vkGetPhysicalDeviceDescriptorSizeEXT"));
|
||||
vkGetDeviceFaultInfoEXT = reinterpret_cast<PFN_vkGetDeviceFaultInfoEXT>(vkGetInstanceProcAddr(instance, "vkGetDeviceFaultInfoEXT"));
|
||||
|
||||
if (memoryDecompressionSupported) {
|
||||
// vkGetDeviceProcAddr skips the loader trampoline that vkGetInstanceProcAddr
|
||||
// requires for device-level functions. The other PFNs above predate this
|
||||
// realization; opportunistic adoption for new entry points only.
|
||||
vkCmdDecompressMemoryEXT = reinterpret_cast<PFN_vkCmdDecompressMemoryEXT>(
|
||||
vkGetDeviceProcAddr(device, "vkCmdDecompressMemoryEXT"));
|
||||
if (vkCmdDecompressMemoryEXT == nullptr) {
|
||||
// Driver advertised the extension but didn't expose the entry
|
||||
// point — defensively fall back to CPU decode.
|
||||
memoryDecompressionSupported = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::uint32_t Device::GetMemoryType(uint32_t typeBits, VkMemoryPropertyFlags properties) {
|
||||
|
|
|
|||
683
implementations/Crafter.Graphics-Gamepad.cpp
Normal file
683
implementations/Crafter.Graphics-Gamepad.cpp
Normal file
|
|
@ -0,0 +1,683 @@
|
|||
/*
|
||||
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 <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
|
||||
#define COBJMACROS
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMINMAX
|
||||
#include <windows.h>
|
||||
#include <windows.gaming.input.h>
|
||||
#include <roapi.h>
|
||||
#include <initguid.h>
|
||||
#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
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// Win32 backend (Windows.Gaming.Input via C ABI)
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
||||
namespace {
|
||||
using IGamepad = __x_ABI_CWindows_CGaming_CInput_CIGamepad;
|
||||
using IGamepadStatics = __x_ABI_CWindows_CGaming_CInput_CIGamepadStatics;
|
||||
using GamepadAddedHandler = __FIEventHandler_1_Windows__CGaming__CInput__CGamepad;
|
||||
using GamepadRemovedHandler = __FIEventHandler_1_Windows__CGaming__CInput__CGamepad;
|
||||
using GamepadReading = __x_ABI_CWindows_CGaming_CInput_CGamepadReading;
|
||||
using GamepadVibration = __x_ABI_CWindows_CGaming_CInput_CGamepadVibration;
|
||||
using GamepadButtons = __x_ABI_CWindows_CGaming_CInput_CGamepadButtons;
|
||||
|
||||
IGamepadStatics* g_statics = nullptr;
|
||||
EventRegistrationToken g_addedToken {};
|
||||
EventRegistrationToken g_removedToken {};
|
||||
bool g_initialized = false;
|
||||
|
||||
// Cross-thread queue. WGI fires Added/Removed on a thread pool; the
|
||||
// event loop drains this on the main thread inside Tick().
|
||||
struct QueueEntry {
|
||||
bool added;
|
||||
IGamepad* gamepad; // AddRef'd
|
||||
};
|
||||
std::mutex g_queueMutex;
|
||||
std::vector<QueueEntry> g_queue;
|
||||
|
||||
// Maps a live IGamepad* to its Crafter::Gamepad::Device. Pointer is
|
||||
// stable from WGI's perspective for the lifetime of the connection.
|
||||
std::unordered_map<IGamepad*, Crafter::Gamepad::Device*> g_byGamepad;
|
||||
|
||||
std::uint32_t PointerToId(IGamepad* g) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
// ── Delegate vtable for GamepadAdded / GamepadRemoved ───────────
|
||||
struct AddedDelegate {
|
||||
// The first member matches the C ABI vtable layout. Cast yields
|
||||
// a valid GamepadAddedHandler*.
|
||||
void* vtbl;
|
||||
LONG refCount;
|
||||
bool isAdded;
|
||||
};
|
||||
|
||||
extern GamepadAddedHandler::__GamepadAddedHandlerVtbl g_addedVtbl;
|
||||
extern GamepadAddedHandler::__GamepadAddedHandlerVtbl g_removedVtbl;
|
||||
|
||||
HRESULT STDMETHODCALLTYPE DelegateQI(GamepadAddedHandler* This, REFIID riid, void** out) {
|
||||
if (IsEqualIID(riid, &IID_IUnknown) || IsEqualIID(riid, &IID_IAgileObject) ||
|
||||
IsEqualIID(riid, &IID___FIEventHandler_1_Windows__CGaming__CInput__CGamepad))
|
||||
{
|
||||
*out = This;
|
||||
This->lpVtbl->AddRef(This);
|
||||
return S_OK;
|
||||
}
|
||||
*out = nullptr;
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE DelegateAddRef(GamepadAddedHandler* This) {
|
||||
AddedDelegate* d = reinterpret_cast<AddedDelegate*>(This);
|
||||
return (ULONG)InterlockedIncrement(&d->refCount);
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE DelegateRelease(GamepadAddedHandler* This) {
|
||||
AddedDelegate* d = reinterpret_cast<AddedDelegate*>(This);
|
||||
LONG r = InterlockedDecrement(&d->refCount);
|
||||
return (ULONG)r;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DelegateInvoke(GamepadAddedHandler* This, IInspectable* sender, IGamepad* gamepad) {
|
||||
AddedDelegate* d = reinterpret_cast<AddedDelegate*>(This);
|
||||
if (gamepad) IGamepad_AddRef(gamepad);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_queueMutex);
|
||||
g_queue.push_back({ d->isAdded, gamepad });
|
||||
}
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
GamepadAddedHandler::__GamepadAddedHandlerVtbl g_addedVtbl = {
|
||||
DelegateQI, DelegateAddRef, DelegateRelease, DelegateInvoke
|
||||
};
|
||||
GamepadAddedHandler::__GamepadAddedHandlerVtbl g_removedVtbl = {
|
||||
DelegateQI, DelegateAddRef, DelegateRelease, DelegateInvoke
|
||||
};
|
||||
|
||||
AddedDelegate g_addedDelegate { &g_addedVtbl, 1, true };
|
||||
AddedDelegate g_removedDelegate { &g_removedVtbl, 1, false };
|
||||
|
||||
// Translate GamepadReading.Buttons bitfield to our Button enum.
|
||||
bool ReadingHas(const GamepadReading& r, GamepadButtons b) {
|
||||
return (r.Buttons & b) != 0;
|
||||
}
|
||||
|
||||
void ApplyReading(Crafter::Gamepad::Device* dev, const GamepadReading& r) {
|
||||
using B = Crafter::Gamepad::Button;
|
||||
using A = Crafter::Gamepad::Axis;
|
||||
|
||||
struct Map { B which; GamepadButtons mask; };
|
||||
static const Map buttonMap[] = {
|
||||
{ B::DPadUp, GamepadButtons_DPadUp },
|
||||
{ B::DPadDown, GamepadButtons_DPadDown },
|
||||
{ B::DPadLeft, GamepadButtons_DPadLeft },
|
||||
{ B::DPadRight, GamepadButtons_DPadRight },
|
||||
{ B::Start, GamepadButtons_Menu },
|
||||
{ B::Select, GamepadButtons_View },
|
||||
{ B::LeftStickClick, GamepadButtons_LeftThumbstick },
|
||||
{ B::RightStickClick,GamepadButtons_RightThumbstick },
|
||||
{ B::LeftBumper, GamepadButtons_LeftShoulder },
|
||||
{ B::RightBumper, GamepadButtons_RightShoulder },
|
||||
{ B::South, GamepadButtons_A },
|
||||
{ B::East, GamepadButtons_B },
|
||||
{ B::West, GamepadButtons_X },
|
||||
{ B::North, GamepadButtons_Y },
|
||||
};
|
||||
for (const Map& m : buttonMap) {
|
||||
bool now = ReadingHas(r, m.mask);
|
||||
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);
|
||||
};
|
||||
axis(A::LeftStickX, (float)r.LeftThumbstickX);
|
||||
axis(A::LeftStickY, (float)r.LeftThumbstickY);
|
||||
axis(A::RightStickX, (float)r.RightThumbstickX);
|
||||
axis(A::RightStickY, (float)r.RightThumbstickY);
|
||||
axis(A::LeftTrigger, (float)r.LeftTrigger);
|
||||
axis(A::RightTrigger, (float)r.RightTrigger);
|
||||
|
||||
// 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);
|
||||
};
|
||||
trigBtn(B::LeftTrigger, r.LeftTrigger);
|
||||
trigBtn(B::RightTrigger, r.RightTrigger);
|
||||
}
|
||||
|
||||
void EnsureInit() {
|
||||
if (g_initialized) return;
|
||||
g_initialized = true;
|
||||
// RoInitialize is required once per thread; ignore RPC_E_CHANGED_MODE
|
||||
// (some hosts initialize as STA, that's fine for WGI).
|
||||
RoInitialize(RO_INIT_MULTITHREADED);
|
||||
|
||||
HSTRING_HEADER header;
|
||||
HSTRING className;
|
||||
const wchar_t* name = RuntimeClass_Windows_Gaming_Input_Gamepad;
|
||||
if (FAILED(WindowsCreateStringReference(name,
|
||||
(UINT32)wcslen(name), &header, &className))) return;
|
||||
|
||||
if (FAILED(RoGetActivationFactory(className,
|
||||
&IID___x_ABI_CWindows_CGaming_CInput_CIGamepadStatics,
|
||||
(void**)&g_statics))) return;
|
||||
|
||||
__x_ABI_CWindows_CGaming_CInput_CIGamepadStatics_add_GamepadAdded(
|
||||
g_statics,
|
||||
reinterpret_cast<GamepadAddedHandler*>(&g_addedDelegate),
|
||||
&g_addedToken);
|
||||
__x_ABI_CWindows_CGaming_CInput_CIGamepadStatics_add_GamepadRemoved(
|
||||
g_statics,
|
||||
reinterpret_cast<GamepadAddedHandler*>(&g_removedDelegate),
|
||||
&g_removedToken);
|
||||
}
|
||||
}
|
||||
|
||||
void Crafter::Gamepad::Tick() {
|
||||
EnsureInit();
|
||||
if (!g_statics) return;
|
||||
|
||||
// Drain the cross-thread connect/disconnect queue.
|
||||
std::vector<QueueEntry> pending;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_queueMutex);
|
||||
pending.swap(g_queue);
|
||||
}
|
||||
for (QueueEntry& e : pending) {
|
||||
if (!e.gamepad) continue;
|
||||
if (e.added) {
|
||||
if (g_byGamepad.find(e.gamepad) != g_byGamepad.end()) {
|
||||
IGamepad_Release(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);
|
||||
// keep AddRef'd; released on 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; }
|
||||
}
|
||||
g_byGamepad.erase(it);
|
||||
}
|
||||
IGamepad_Release(e.gamepad);
|
||||
}
|
||||
}
|
||||
|
||||
// Poll each connected gamepad.
|
||||
for (auto& [gp, dev] : g_byGamepad) {
|
||||
GamepadReading reading{};
|
||||
if (FAILED(__x_ABI_CWindows_CGaming_CInput_CIGamepad_GetCurrentReading(gp, &reading))) continue;
|
||||
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.
|
||||
IGamepad* gp = nullptr;
|
||||
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; };
|
||||
GamepadVibration vib{};
|
||||
vib.LeftMotor = clamp01(low);
|
||||
vib.RightMotor = clamp01(high);
|
||||
__x_ABI_CWindows_CGaming_CInput_CIGamepad_put_Vibration(gp, vib);
|
||||
}
|
||||
#endif
|
||||
|
||||
Crafter::Gamepad::Device* Crafter::Gamepad::FindById(std::uint32_t id) {
|
||||
for (auto& up : connected) {
|
||||
if (up->id == id) return up.get();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
454
implementations/Crafter.Graphics-Input.cpp
Normal file
454
implementations/Crafter.Graphics-Input.cpp
Normal file
|
|
@ -0,0 +1,454 @@
|
|||
/*
|
||||
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;
|
||||
module Crafter.Graphics;
|
||||
import std;
|
||||
import Crafter.Math;
|
||||
import Crafter.Event;
|
||||
|
||||
using namespace Crafter;
|
||||
using namespace Crafter::Input;
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// Helpers
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
namespace {
|
||||
// Look up a connected gamepad by stable id. Returns nullptr if not
|
||||
// currently connected — the binding becomes inert until reconnect.
|
||||
Gamepad::Device* FindGamepad(std::uint32_t id) {
|
||||
return Gamepad::FindById(id);
|
||||
}
|
||||
|
||||
float ApplyDeadzone(float v, float dz) {
|
||||
float a = std::abs(v);
|
||||
if (a <= dz) return 0.0f;
|
||||
float sign = v < 0 ? -1.0f : 1.0f;
|
||||
return sign * (a - dz) / (1.0f - dz);
|
||||
}
|
||||
|
||||
Vector<float, 2> ApplyRadialDeadzone(Vector<float, 2> v, float dz) {
|
||||
float mag = std::sqrt(v.x * v.x + v.y * v.y);
|
||||
if (mag <= dz) return Vector<float, 2>{0.0f, 0.0f};
|
||||
float scaled = (mag - dz) / (1.0f - dz);
|
||||
float k = scaled / mag;
|
||||
return Vector<float, 2>{ v.x * k, v.y * k };
|
||||
}
|
||||
|
||||
// ─── Evaluate one binding to its raw contribution. ───────────────
|
||||
// Buttons: bool. Axis: float. Vector2: pair.
|
||||
struct Eval {
|
||||
bool button = false;
|
||||
float value = 0.0f;
|
||||
Vector<float, 2> vec { 0.0f, 0.0f };
|
||||
};
|
||||
|
||||
Eval EvalBinding(const Binding& b, Window* w, std::int32_t scrollAccum,
|
||||
Vector<float, 2> mouseDelta, float deadzone)
|
||||
{
|
||||
Eval r;
|
||||
std::visit([&](auto const& bb) {
|
||||
using T = std::decay_t<decltype(bb)>;
|
||||
if constexpr (std::is_same_v<T, KeyBind>) {
|
||||
if (w && w->heldKeys.contains(bb.code)) r.button = true;
|
||||
r.value = r.button ? 1.0f : 0.0f;
|
||||
} else if constexpr (std::is_same_v<T, MouseButtonBind>) {
|
||||
if (w) {
|
||||
if (bb.button == 0 && w->mouseLeftHeld) r.button = true;
|
||||
if (bb.button == 1 && w->mouseRightHeld) r.button = true;
|
||||
}
|
||||
r.value = r.button ? 1.0f : 0.0f;
|
||||
} else if constexpr (std::is_same_v<T, MouseScrollBind>) {
|
||||
r.value = (float)scrollAccum;
|
||||
r.button = scrollAccum != 0;
|
||||
} else if constexpr (std::is_same_v<T, MouseDeltaBind>) {
|
||||
r.vec = Vector<float, 2>{
|
||||
mouseDelta.x * bb.scale,
|
||||
mouseDelta.y * bb.scale
|
||||
};
|
||||
} else if constexpr (std::is_same_v<T, GamepadButtonBind>) {
|
||||
auto* gp = FindGamepad(bb.gamepadId);
|
||||
if (gp) r.button = gp->buttons[(std::size_t)bb.button];
|
||||
r.value = r.button ? 1.0f : 0.0f;
|
||||
} else if constexpr (std::is_same_v<T, GamepadAxisBind>) {
|
||||
auto* gp = FindGamepad(bb.gamepadId);
|
||||
if (gp) {
|
||||
float v = gp->axes[(std::size_t)bb.axis];
|
||||
if (bb.invert) v = -v;
|
||||
// Triggers (0..1) and sticks (-1..1) share this path;
|
||||
// deadzone applies symmetrically.
|
||||
r.value = ApplyDeadzone(v, deadzone);
|
||||
r.button = std::abs(r.value) > 0.0f;
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, GamepadStickBind>) {
|
||||
auto* gp = FindGamepad(bb.gamepadId);
|
||||
if (gp) {
|
||||
using A = Gamepad::Axis;
|
||||
A xAxis = (bb.stick == Gamepad::Stick::Left) ? A::LeftStickX : A::RightStickX;
|
||||
A yAxis = (bb.stick == Gamepad::Stick::Left) ? A::LeftStickY : A::RightStickY;
|
||||
Vector<float, 2> raw{
|
||||
gp->axes[(std::size_t)xAxis],
|
||||
gp->axes[(std::size_t)yAxis]
|
||||
};
|
||||
r.vec = ApplyRadialDeadzone(raw, deadzone);
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, WASDBind>) {
|
||||
Vector<float, 2> v{ 0.0f, 0.0f };
|
||||
if (w) {
|
||||
if (w->heldKeys.contains(bb.left)) v.x -= 1.0f;
|
||||
if (w->heldKeys.contains(bb.right)) v.x += 1.0f;
|
||||
if (w->heldKeys.contains(bb.up)) v.y += 1.0f;
|
||||
if (w->heldKeys.contains(bb.down)) v.y -= 1.0f;
|
||||
}
|
||||
r.vec = v;
|
||||
}
|
||||
}, b);
|
||||
return r;
|
||||
}
|
||||
|
||||
// Combine bindings for the action's type. For Button: OR. For Axis1D:
|
||||
// sum-then-clamp (so two analog inputs add but cap at ±1; two
|
||||
// digital inputs cap at 1 too). For Vector2: per-axis sum-and-clamp.
|
||||
void EvaluateAction(Action& a, Window* w, std::int32_t scrollAccum,
|
||||
Vector<float, 2> mouseDelta)
|
||||
{
|
||||
bool nextPressed = false;
|
||||
float nextValue = 0.0f;
|
||||
Vector<float, 2> nextVec { 0.0f, 0.0f };
|
||||
|
||||
for (const Binding& b : a.bindings) {
|
||||
Eval e = EvalBinding(b, w, scrollAccum, mouseDelta, a.deadzone);
|
||||
if (e.button) nextPressed = true;
|
||||
nextValue += e.value;
|
||||
nextVec.x += e.vec.x;
|
||||
nextVec.y += e.vec.y;
|
||||
}
|
||||
// Clamp scalars/vectors to ±1; sum semantics for combining.
|
||||
auto clamp = [](float v) { return v < -1.0f ? -1.0f : v > 1.0f ? 1.0f : v; };
|
||||
nextValue = clamp(nextValue);
|
||||
nextVec.x = clamp(nextVec.x);
|
||||
nextVec.y = clamp(nextVec.y);
|
||||
|
||||
// Dispatch edges + value changes.
|
||||
if (a.type == ActionType::Button) {
|
||||
if (nextPressed && !a.pressed) {
|
||||
a.pressed = true;
|
||||
a.onPerformed.Invoke();
|
||||
} else if (!nextPressed && a.pressed) {
|
||||
a.pressed = false;
|
||||
a.onCanceled.Invoke();
|
||||
}
|
||||
} else if (a.type == ActionType::Axis1D) {
|
||||
bool wasNonZero = a.value != 0.0f;
|
||||
bool isNonZero = nextValue != 0.0f;
|
||||
if (nextValue != a.value) {
|
||||
a.value = nextValue;
|
||||
a.onValueChanged.Invoke(nextValue);
|
||||
}
|
||||
if (isNonZero && !wasNonZero) a.onPerformed.Invoke();
|
||||
else if (!isNonZero && wasNonZero) a.onCanceled.Invoke();
|
||||
} else { // Vector2
|
||||
bool wasNonZero = (a.vector2.x != 0.0f || a.vector2.y != 0.0f);
|
||||
bool isNonZero = (nextVec.x != 0.0f || nextVec.y != 0.0f);
|
||||
if (nextVec.x != a.vector2.x || nextVec.y != a.vector2.y) {
|
||||
a.vector2 = nextVec;
|
||||
a.onVector2Changed.Invoke(nextVec);
|
||||
}
|
||||
if (isNonZero && !wasNonZero) a.onPerformed.Invoke();
|
||||
else if (!isNonZero && wasNonZero) a.onCanceled.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Rebind detection ───────────────────────────────────────────
|
||||
// Scan for a fresh down-edge against the snapshot taken at
|
||||
// StartRebind. Returns the captured binding if found, nullopt if
|
||||
// nothing fresh is held. The first match wins; preference goes
|
||||
// keyboard > mouse > gamepad to keep the behavior predictable.
|
||||
std::optional<Binding> DetectRebindEdge(Map::RebindState& s, Window* w) {
|
||||
// Keyboard
|
||||
if (w && HasFlag(s.mask, CaptureMask::Keyboard)) {
|
||||
for (KeyCode c : w->heldKeys) {
|
||||
if (!s.keysHeldAtStart.contains(c)) {
|
||||
return Binding{ KeyBind{c} };
|
||||
}
|
||||
}
|
||||
}
|
||||
// Mouse buttons
|
||||
if (w && HasFlag(s.mask, CaptureMask::Mouse)) {
|
||||
if (w->mouseLeftHeld && !s.mouseLeftHeldAtStart) {
|
||||
return Binding{ MouseButtonBind{0} };
|
||||
}
|
||||
if (w->mouseRightHeld && !s.mouseRightHeldAtStart) {
|
||||
return Binding{ MouseButtonBind{1} };
|
||||
}
|
||||
}
|
||||
// Gamepad buttons + axes
|
||||
if (HasFlag(s.mask, CaptureMask::Gamepad)) {
|
||||
for (auto& up : Gamepad::connected) {
|
||||
Gamepad::Device* gp = up.get();
|
||||
auto it = s.gamepadButtonsAtStart.find(gp->id);
|
||||
bool hadSnapshot = it != s.gamepadButtonsAtStart.end();
|
||||
for (std::size_t i = 0; i < (std::size_t)Gamepad::Button::Max; ++i) {
|
||||
bool now = gp->buttons[i];
|
||||
bool before = hadSnapshot ? it->second[i] : false;
|
||||
if (now && !before) {
|
||||
return Binding{ GamepadButtonBind{
|
||||
gp->id, (Gamepad::Button)i
|
||||
} };
|
||||
}
|
||||
}
|
||||
// Axes: any value past 0.5 captures as a 1-D axis bind.
|
||||
constexpr float kRebindAxisThreshold = 0.5f;
|
||||
for (std::size_t i = 0; i < (std::size_t)Gamepad::Axis::Max; ++i) {
|
||||
float v = gp->axes[i];
|
||||
if (std::abs(v) >= kRebindAxisThreshold) {
|
||||
return Binding{ GamepadAxisBind{
|
||||
gp->id, (Gamepad::Axis)i, v < 0.0f
|
||||
} };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// Map
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
Action& Map::AddAction(std::string name, ActionType type) {
|
||||
auto up = std::make_unique<Action>();
|
||||
up->name = std::move(name);
|
||||
up->type = type;
|
||||
Action& ref = *up;
|
||||
actions.push_back(std::move(up));
|
||||
return ref;
|
||||
}
|
||||
|
||||
Action* Map::Find(std::string_view name) {
|
||||
for (auto& up : actions) {
|
||||
if (up->name == name) return up.get();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Map::Attach(Window& w) {
|
||||
Detach();
|
||||
window = &w;
|
||||
// Only event-driven source we need is mouse scroll — everything
|
||||
// else (held keys, mouse buttons, mouse position, gamepad state) is
|
||||
// polled directly. Accumulate scroll between ticks so a flick that
|
||||
// fires multiple wheel events in one frame all count.
|
||||
scrollListener = std::make_unique<EventListener<std::uint32_t>>(
|
||||
&w.onMouseScroll,
|
||||
[this](std::uint32_t delta) {
|
||||
// Wayland and Win32 both deliver scroll as a signed delta
|
||||
// packed into a uint32; we reinterpret to preserve sign.
|
||||
scrollAccumulator += (std::int32_t)delta;
|
||||
});
|
||||
lastMousePos.reset();
|
||||
scrollAccumulator = 0;
|
||||
}
|
||||
|
||||
void Map::Detach() {
|
||||
if (scrollListener) {
|
||||
scrollListener->Clear();
|
||||
scrollListener.reset();
|
||||
}
|
||||
window = nullptr;
|
||||
scrollAccumulator = 0;
|
||||
lastMousePos.reset();
|
||||
rebind.reset();
|
||||
}
|
||||
|
||||
void Map::Tick() {
|
||||
// Mouse delta: derived from window position vs. last frame.
|
||||
Vector<float, 2> mouseDelta{ 0.0f, 0.0f };
|
||||
if (window) {
|
||||
if (lastMousePos.has_value()) {
|
||||
mouseDelta.x = window->currentMousePos.x - lastMousePos->x;
|
||||
mouseDelta.y = window->currentMousePos.y - lastMousePos->y;
|
||||
}
|
||||
lastMousePos = window->currentMousePos;
|
||||
}
|
||||
|
||||
// Rebind takes priority. If a fresh edge is detected, fire the
|
||||
// callback and exit rebind mode. Actions don't evaluate this tick
|
||||
// for the captured input — the user usually wants the bind to take
|
||||
// effect on the NEXT press of that input.
|
||||
if (rebind.has_value()) {
|
||||
if (auto captured = DetectRebindEdge(*rebind, window)) {
|
||||
auto cb = std::move(rebind->onCaptured);
|
||||
rebind.reset();
|
||||
// Drain scroll so the next Tick doesn't double-fire on the
|
||||
// same wheel input that may have triggered the rebind.
|
||||
scrollAccumulator = 0;
|
||||
cb(*captured);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
std::int32_t scrollThisTick = scrollAccumulator;
|
||||
scrollAccumulator = 0;
|
||||
|
||||
for (auto& up : actions) {
|
||||
EvaluateAction(*up, window, scrollThisTick, mouseDelta);
|
||||
}
|
||||
}
|
||||
|
||||
void Map::StartRebind(Action& action, CaptureMask mask,
|
||||
std::function<void(Binding)> onCaptured)
|
||||
{
|
||||
RebindState s;
|
||||
s.action = &action;
|
||||
s.mask = mask;
|
||||
s.onCaptured = std::move(onCaptured);
|
||||
if (window) {
|
||||
s.keysHeldAtStart = window->heldKeys;
|
||||
s.mouseLeftHeldAtStart = window->mouseLeftHeld;
|
||||
s.mouseRightHeldAtStart = window->mouseRightHeld;
|
||||
}
|
||||
for (auto& up : Gamepad::connected) {
|
||||
std::array<bool, (std::size_t)Gamepad::Button::Max> snap{};
|
||||
for (std::size_t i = 0; i < snap.size(); ++i) {
|
||||
snap[i] = up->buttons[i];
|
||||
}
|
||||
s.gamepadButtonsAtStart[up->id] = snap;
|
||||
}
|
||||
rebind.emplace(std::move(s));
|
||||
}
|
||||
|
||||
void Map::StopRebind() {
|
||||
rebind.reset();
|
||||
}
|
||||
|
||||
bool Map::IsRebinding() const {
|
||||
return rebind.has_value();
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// Serialization
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
std::string Crafter::Input::BindingToString(const Binding& b) {
|
||||
std::string out;
|
||||
std::visit([&](auto const& bb) {
|
||||
using T = std::decay_t<decltype(bb)>;
|
||||
if constexpr (std::is_same_v<T, KeyBind>) {
|
||||
out = std::format("key:{:x}", bb.code);
|
||||
} else if constexpr (std::is_same_v<T, MouseButtonBind>) {
|
||||
out = std::format("mb:{}", (int)bb.button);
|
||||
} else if constexpr (std::is_same_v<T, MouseScrollBind>) {
|
||||
out = "mscroll";
|
||||
} else if constexpr (std::is_same_v<T, MouseDeltaBind>) {
|
||||
out = std::format("mdelta:{}", bb.scale);
|
||||
} else if constexpr (std::is_same_v<T, GamepadButtonBind>) {
|
||||
out = std::format("gpb:{}:{}", bb.gamepadId, (int)bb.button);
|
||||
} else if constexpr (std::is_same_v<T, GamepadAxisBind>) {
|
||||
out = std::format("gpa:{}:{}:{}", bb.gamepadId, (int)bb.axis, bb.invert ? 1 : 0);
|
||||
} else if constexpr (std::is_same_v<T, GamepadStickBind>) {
|
||||
out = std::format("gps:{}:{}", bb.gamepadId, (int)bb.stick);
|
||||
} else if constexpr (std::is_same_v<T, WASDBind>) {
|
||||
out = std::format("wasd:{:x}:{:x}:{:x}:{:x}",
|
||||
bb.up, bb.down, bb.left, bb.right);
|
||||
}
|
||||
}, b);
|
||||
return out;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Split on ':' and parse. Tolerates leading whitespace; rejects
|
||||
// malformed input by returning nullopt.
|
||||
std::vector<std::string_view> Split(std::string_view s) {
|
||||
std::vector<std::string_view> parts;
|
||||
std::size_t start = 0;
|
||||
for (std::size_t i = 0; i < s.size(); ++i) {
|
||||
if (s[i] == ':') {
|
||||
parts.emplace_back(s.substr(start, i - start));
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
parts.emplace_back(s.substr(start));
|
||||
return parts;
|
||||
}
|
||||
|
||||
bool ParseHex(std::string_view s, std::uint32_t& out) {
|
||||
auto [p, ec] = std::from_chars(s.data(), s.data() + s.size(), out, 16);
|
||||
return ec == std::errc{} && p == s.data() + s.size();
|
||||
}
|
||||
bool ParseDec(std::string_view s, std::uint32_t& out) {
|
||||
auto [p, ec] = std::from_chars(s.data(), s.data() + s.size(), out, 10);
|
||||
return ec == std::errc{} && p == s.data() + s.size();
|
||||
}
|
||||
bool ParseFloat(std::string_view s, float& out) {
|
||||
auto [p, ec] = std::from_chars(s.data(), s.data() + s.size(), out);
|
||||
return ec == std::errc{} && p == s.data() + s.size();
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<Binding> Crafter::Input::BindingFromString(std::string_view s) {
|
||||
auto parts = Split(s);
|
||||
if (parts.empty()) return std::nullopt;
|
||||
const std::string_view tag = parts[0];
|
||||
|
||||
if (tag == "key" && parts.size() == 2) {
|
||||
std::uint32_t v;
|
||||
if (!ParseHex(parts[1], v)) return std::nullopt;
|
||||
return Binding{ KeyBind{ v } };
|
||||
}
|
||||
if (tag == "mb" && parts.size() == 2) {
|
||||
std::uint32_t v;
|
||||
if (!ParseDec(parts[1], v) || v > 255) return std::nullopt;
|
||||
return Binding{ MouseButtonBind{ (std::uint8_t)v } };
|
||||
}
|
||||
if (tag == "mscroll") return Binding{ MouseScrollBind{} };
|
||||
if (tag == "mdelta" && parts.size() == 2) {
|
||||
float scale;
|
||||
if (!ParseFloat(parts[1], scale)) return std::nullopt;
|
||||
return Binding{ MouseDeltaBind{ scale } };
|
||||
}
|
||||
if (tag == "gpb" && parts.size() == 3) {
|
||||
std::uint32_t id, btn;
|
||||
if (!ParseDec(parts[1], id) || !ParseDec(parts[2], btn)) return std::nullopt;
|
||||
if (btn >= (std::uint32_t)Gamepad::Button::Max) return std::nullopt;
|
||||
return Binding{ GamepadButtonBind{ id, (Gamepad::Button)btn } };
|
||||
}
|
||||
if (tag == "gpa" && parts.size() == 4) {
|
||||
std::uint32_t id, ax, inv;
|
||||
if (!ParseDec(parts[1], id) || !ParseDec(parts[2], ax) || !ParseDec(parts[3], inv)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (ax >= (std::uint32_t)Gamepad::Axis::Max) return std::nullopt;
|
||||
return Binding{ GamepadAxisBind{ id, (Gamepad::Axis)ax, inv != 0 } };
|
||||
}
|
||||
if (tag == "gps" && parts.size() == 3) {
|
||||
std::uint32_t id, st;
|
||||
if (!ParseDec(parts[1], id) || !ParseDec(parts[2], st)) return std::nullopt;
|
||||
if (st > 1) return std::nullopt;
|
||||
return Binding{ GamepadStickBind{ id, (Gamepad::Stick)st } };
|
||||
}
|
||||
if (tag == "wasd" && parts.size() == 5) {
|
||||
std::uint32_t u, d, l, r;
|
||||
if (!ParseHex(parts[1], u) || !ParseHex(parts[2], d) ||
|
||||
!ParseHex(parts[3], l) || !ParseHex(parts[4], r)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return Binding{ WASDBind{ u, d, l, r } };
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ import :UI;
|
|||
import :UIComponents;
|
||||
import :Font;
|
||||
import :Types;
|
||||
import :Keys;
|
||||
import std;
|
||||
|
||||
using namespace Crafter;
|
||||
|
|
@ -87,32 +88,26 @@ void Crafter::InputField_OnText(InputField& f, std::string_view utf8) {
|
|||
f.cursorPos += utf8.size();
|
||||
}
|
||||
|
||||
void Crafter::InputField_OnKey(InputField& f, CrafterKeys key) {
|
||||
switch (key) {
|
||||
case CrafterKeys::Backspace:
|
||||
void Crafter::InputField_OnKey(InputField& f, KeyCode code) {
|
||||
// `Key(...)` is consteval — every comparison below folds to an immediate
|
||||
// integer at compile time, so this is just a sequence of int-eq tests.
|
||||
if (code == Key(CrafterKeys::Backspace)) {
|
||||
if (f.cursorPos > 0 && !f.value.empty()) {
|
||||
f.value.erase(f.cursorPos - 1, 1);
|
||||
--f.cursorPos;
|
||||
}
|
||||
break;
|
||||
case CrafterKeys::Delete:
|
||||
} else if (code == Key(CrafterKeys::Delete)) {
|
||||
if (f.cursorPos < f.value.size()) {
|
||||
f.value.erase(f.cursorPos, 1);
|
||||
}
|
||||
break;
|
||||
case CrafterKeys::Left:
|
||||
} else if (code == Key(CrafterKeys::Left)) {
|
||||
if (f.cursorPos > 0) --f.cursorPos;
|
||||
break;
|
||||
case CrafterKeys::Right:
|
||||
} else if (code == Key(CrafterKeys::Right)) {
|
||||
if (f.cursorPos < f.value.size()) ++f.cursorPos;
|
||||
break;
|
||||
case CrafterKeys::Home:
|
||||
} else if (code == Key(CrafterKeys::Home)) {
|
||||
f.cursorPos = 0;
|
||||
break;
|
||||
case CrafterKeys::End:
|
||||
} else if (code == Key(CrafterKeys::End)) {
|
||||
f.cursorPos = f.value.size();
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,35 +21,37 @@ module;
|
|||
#include "vulkan/vulkan.h"
|
||||
module Crafter.Graphics:Mesh_impl;
|
||||
import Crafter.Math;
|
||||
import Crafter.Asset;
|
||||
import :Mesh;
|
||||
import :Device;
|
||||
import :Decompress;
|
||||
import :Types;
|
||||
import std;
|
||||
|
||||
using namespace Crafter;
|
||||
|
||||
void Mesh::Build(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, VkCommandBuffer cmd) {
|
||||
vertexBuffer.Resize(VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_2_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, verticies.size());
|
||||
indexBuffer.Resize(VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_2_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, indicies.size());
|
||||
|
||||
std::memcpy(vertexBuffer.value, verticies.data(), verticies.size() * sizeof(Vector<float, 3, 3>));
|
||||
std::memcpy(indexBuffer.value, indicies.data(), indicies.size() * sizeof(std::uint32_t));
|
||||
|
||||
vertexBuffer.FlushDevice(cmd, VK_ACCESS_MEMORY_READ_BIT, VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR);
|
||||
indexBuffer.FlushDevice(cmd, VK_ACCESS_MEMORY_READ_BIT, VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR);
|
||||
namespace {
|
||||
// Buffer-usage flag set shared by both Build paths. The compressed path
|
||||
// appends VK_BUFFER_USAGE_2_MEMORY_DECOMPRESSION_BIT_EXT.
|
||||
constexpr VkBufferUsageFlags2 kVertexUsageBase =
|
||||
VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT
|
||||
| VK_BUFFER_USAGE_2_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR;
|
||||
constexpr VkBufferUsageFlags2 kIndexUsageBase =
|
||||
kVertexUsageBase | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
|
||||
|
||||
void RecordBLASBuild(Mesh& self, std::uint32_t vertexCount, std::uint32_t indexCount, VkCommandBuffer cmd) {
|
||||
VkDeviceOrHostAddressConstKHR vertexAddr;
|
||||
vertexAddr.deviceAddress = vertexBuffer.address;
|
||||
vertexAddr.deviceAddress = self.vertexBuffer.address;
|
||||
|
||||
VkDeviceOrHostAddressConstKHR indexAddr;
|
||||
indexAddr.deviceAddress = indexBuffer.address;
|
||||
indexAddr.deviceAddress = self.indexBuffer.address;
|
||||
|
||||
auto trianglesData = VkAccelerationStructureGeometryTrianglesDataKHR {
|
||||
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_TRIANGLES_DATA_KHR,
|
||||
.vertexFormat = VK_FORMAT_R32G32B32_SFLOAT,
|
||||
.vertexData = vertexAddr,
|
||||
.vertexStride = sizeof(Vector<float, 3, 3>),
|
||||
.maxVertex = static_cast<std::uint32_t>(verticies.size())-1,
|
||||
.maxVertex = vertexCount - 1,
|
||||
.indexType = VK_INDEX_TYPE_UINT32,
|
||||
.indexData = indexAddr,
|
||||
.transformData = {.deviceAddress = 0}
|
||||
|
|
@ -70,13 +72,10 @@ void Mesh::Build(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32
|
|||
.pGeometries = &blasGeometry,
|
||||
};
|
||||
|
||||
// Query the memory sizes that will be needed for this BLAS
|
||||
auto primitiveCount = static_cast<uint32_t>(indicies.size() / 3);
|
||||
|
||||
auto primitiveCount = indexCount / 3;
|
||||
VkAccelerationStructureBuildSizesInfoKHR blasBuildSizes = {
|
||||
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_BUILD_SIZES_INFO_KHR
|
||||
};
|
||||
|
||||
Device::vkGetAccelerationStructureBuildSizesKHR(
|
||||
Device::device,
|
||||
VK_ACCELERATION_STRUCTURE_BUILD_TYPE_DEVICE_KHR,
|
||||
|
|
@ -85,44 +84,122 @@ void Mesh::Build(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32
|
|||
&blasBuildSizes
|
||||
);
|
||||
|
||||
scratchBuffer.Resize(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, blasBuildSizes.buildScratchSize);
|
||||
self.scratchBuffer.Resize(
|
||||
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
|
||||
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
|
||||
blasBuildSizes.buildScratchSize);
|
||||
blasBuildGeometryInfo.scratchData.deviceAddress = self.scratchBuffer.address;
|
||||
|
||||
VkPhysicalDeviceAccelerationStructurePropertiesKHR asProps{
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ACCELERATION_STRUCTURE_PROPERTIES_KHR
|
||||
};
|
||||
self.blasBuffer.Resize(
|
||||
VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_STORAGE_BIT_KHR
|
||||
| VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT
|
||||
| VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR,
|
||||
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
|
||||
blasBuildSizes.accelerationStructureSize);
|
||||
|
||||
VkDeviceAddress scratchAddr = scratchBuffer.address;
|
||||
blasBuildGeometryInfo.scratchData.deviceAddress = scratchAddr;
|
||||
|
||||
blasBuffer.Resize(VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_STORAGE_BIT_KHR | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, blasBuildSizes.accelerationStructureSize);
|
||||
|
||||
// Create and store the BLAS handle
|
||||
VkAccelerationStructureCreateInfoKHR blasCreateInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_CREATE_INFO_KHR,
|
||||
.buffer = blasBuffer.buffer,
|
||||
.buffer = self.blasBuffer.buffer,
|
||||
.offset = 0,
|
||||
.size = blasBuildSizes.accelerationStructureSize,
|
||||
.type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR,
|
||||
};
|
||||
Device::CheckVkResult(Device::vkCreateAccelerationStructureKHR(Device::device, &blasCreateInfo, nullptr, &self.accelerationStructure));
|
||||
blasBuildGeometryInfo.dstAccelerationStructure = self.accelerationStructure;
|
||||
|
||||
|
||||
Device::CheckVkResult(Device::vkCreateAccelerationStructureKHR(Device::device, &blasCreateInfo, nullptr, &accelerationStructure));
|
||||
blasBuildGeometryInfo.dstAccelerationStructure = accelerationStructure;
|
||||
|
||||
// Prepare the build range for the BLAS
|
||||
VkAccelerationStructureBuildRangeInfoKHR blasRangeInfo {
|
||||
.primitiveCount = primitiveCount,
|
||||
.primitiveOffset = 0,
|
||||
.firstVertex = 0,
|
||||
.transformOffset = 0
|
||||
};
|
||||
|
||||
VkAccelerationStructureBuildRangeInfoKHR* blasRangeInfoPP = &blasRangeInfo;
|
||||
Device::vkCmdBuildAccelerationStructuresKHR(cmd, 1, &blasBuildGeometryInfo, &blasRangeInfoPP);
|
||||
|
||||
VkAccelerationStructureDeviceAddressInfoKHR addrInfo {
|
||||
.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_DEVICE_ADDRESS_INFO_KHR,
|
||||
.accelerationStructure = accelerationStructure
|
||||
.accelerationStructure = self.accelerationStructure
|
||||
};
|
||||
blasAddr = Device::vkGetAccelerationStructureDeviceAddressKHR(Device::device, &addrInfo);
|
||||
self.blasAddr = Device::vkGetAccelerationStructureDeviceAddressKHR(Device::device, &addrInfo);
|
||||
}
|
||||
}
|
||||
|
||||
void Mesh::Build(std::span<Vector<float, 3, 3>> verticies, std::span<std::uint32_t> indicies, VkCommandBuffer cmd) {
|
||||
vertexBuffer.Resize(kVertexUsageBase, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, verticies.size());
|
||||
indexBuffer.Resize(kIndexUsageBase, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, indicies.size());
|
||||
|
||||
std::memcpy(vertexBuffer.value, verticies.data(), verticies.size() * sizeof(Vector<float, 3, 3>));
|
||||
std::memcpy(indexBuffer.value, indicies.data(), indicies.size() * sizeof(std::uint32_t));
|
||||
|
||||
vertexBuffer.FlushDevice(cmd, VK_ACCESS_MEMORY_READ_BIT, VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR);
|
||||
indexBuffer.FlushDevice(cmd, VK_ACCESS_MEMORY_READ_BIT, VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR);
|
||||
|
||||
RecordBLASBuild(*this, static_cast<std::uint32_t>(verticies.size()), static_cast<std::uint32_t>(indicies.size()), cmd);
|
||||
}
|
||||
|
||||
void Mesh::Build(const CompressedMeshAsset& asset, VkCommandBuffer cmd) {
|
||||
if (!Device::memoryDecompressionSupported) {
|
||||
// CPU fallback: decompress into temporary host vectors, then take
|
||||
// the existing uncompressed path. The data region is decompressed
|
||||
// into a discard buffer (consumer is expected to handle data-stream
|
||||
// decoding via Compression::DecompressCPU on its own buffer).
|
||||
std::vector<Vector<float, 3, 3>> vertices(asset.vertexCount);
|
||||
std::vector<std::uint32_t> indices(asset.indexCount);
|
||||
std::vector<std::byte> dataDiscard(
|
||||
asset.blob.regions.size() >= 3 ? asset.blob.regions[2].decompressedSize : 0);
|
||||
std::array<std::span<std::byte>, 3> outputs = {
|
||||
std::as_writable_bytes(std::span(vertices)),
|
||||
std::as_writable_bytes(std::span(indices)),
|
||||
std::span<std::byte>(dataDiscard),
|
||||
};
|
||||
Compression::DecompressCPU(asset.blob, std::span(outputs).first(asset.blob.regions.size()));
|
||||
Build(vertices, indices, cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
vertexBuffer.Resize(
|
||||
kVertexUsageBase | VK_BUFFER_USAGE_2_MEMORY_DECOMPRESSION_BIT_EXT,
|
||||
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
|
||||
asset.vertexCount);
|
||||
indexBuffer.Resize(
|
||||
kIndexUsageBase | VK_BUFFER_USAGE_2_MEMORY_DECOMPRESSION_BIT_EXT,
|
||||
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
|
||||
asset.indexCount);
|
||||
|
||||
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::array<VkDeviceAddress, 3> dstAddresses = {
|
||||
vertexBuffer.address,
|
||||
indexBuffer.address,
|
||||
0, // data region is not consumed by Mesh; caller handles it separately.
|
||||
};
|
||||
std::vector<VkDecompressMemoryRegionEXT> regions;
|
||||
for (std::size_t i = 0; i < asset.blob.regions.size() && i < 2; ++i) {
|
||||
const Compression::RegionMeta& r = asset.blob.regions[i];
|
||||
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,
|
||||
dstAddresses[i],
|
||||
regions);
|
||||
}
|
||||
|
||||
Decompress::DecompressOnGPU(
|
||||
cmd,
|
||||
regions,
|
||||
VK_PIPELINE_STAGE_2_ACCELERATION_STRUCTURE_BUILD_BIT_KHR,
|
||||
VK_ACCESS_2_ACCELERATION_STRUCTURE_READ_BIT_KHR);
|
||||
|
||||
RecordBLASBuild(*this, asset.vertexCount, asset.indexCount, cmd);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,14 @@ void UIRenderer::Initialize(Window& window, DescriptorHeapVulkan& heap, VkComman
|
|||
for (auto& h : heap_->resourceHeap) h.FlushDevice();
|
||||
for (auto& h : heap_->samplerHeap) h.FlushDevice();
|
||||
|
||||
// Re-bind the swapchain image descriptors after every resize. Window
|
||||
// already drained the queue and rebuilt the imageViews[] before
|
||||
// firing the event, so vkWriteResourceDescriptorsEXT is safe here.
|
||||
resizeSub_.SetEvent(&window.onResize, [this]() {
|
||||
WriteSwapchainDescriptors();
|
||||
for (auto& h : heap_->resourceHeap) h.FlushDevice();
|
||||
});
|
||||
|
||||
(void)initCmd; // reserved for future image-layout tweaks
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ module;
|
|||
module Crafter.Graphics:Window_impl;
|
||||
import :Window;
|
||||
import :Device;
|
||||
import :Gamepad;
|
||||
import :VulkanTransition;
|
||||
import :DescriptorHeapVulkan;
|
||||
import :RenderPass;
|
||||
|
|
@ -110,131 +111,15 @@ int create_shm_file(off_t size) {
|
|||
#endif
|
||||
|
||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
||||
CrafterKeys vk_to_crafter_key(WPARAM vk)
|
||||
{
|
||||
switch (vk)
|
||||
{
|
||||
// Alphabet
|
||||
case 'A': return CrafterKeys::A;
|
||||
case 'B': return CrafterKeys::B;
|
||||
case 'C': return CrafterKeys::C;
|
||||
case 'D': return CrafterKeys::D;
|
||||
case 'E': return CrafterKeys::E;
|
||||
case 'F': return CrafterKeys::F;
|
||||
case 'G': return CrafterKeys::G;
|
||||
case 'H': return CrafterKeys::H;
|
||||
case 'I': return CrafterKeys::I;
|
||||
case 'J': return CrafterKeys::J;
|
||||
case 'K': return CrafterKeys::K;
|
||||
case 'L': return CrafterKeys::L;
|
||||
case 'M': return CrafterKeys::M;
|
||||
case 'N': return CrafterKeys::N;
|
||||
case 'O': return CrafterKeys::O;
|
||||
case 'P': return CrafterKeys::P;
|
||||
case 'Q': return CrafterKeys::Q;
|
||||
case 'R': return CrafterKeys::R;
|
||||
case 'S': return CrafterKeys::S;
|
||||
case 'T': return CrafterKeys::T;
|
||||
case 'U': return CrafterKeys::U;
|
||||
case 'V': return CrafterKeys::V;
|
||||
case 'W': return CrafterKeys::W;
|
||||
case 'X': return CrafterKeys::X;
|
||||
case 'Y': return CrafterKeys::Y;
|
||||
case 'Z': return CrafterKeys::Z;
|
||||
|
||||
// Numbers
|
||||
case '0': return CrafterKeys::_0;
|
||||
case '1': return CrafterKeys::_1;
|
||||
case '2': return CrafterKeys::_2;
|
||||
case '3': return CrafterKeys::_3;
|
||||
case '4': return CrafterKeys::_4;
|
||||
case '5': return CrafterKeys::_5;
|
||||
case '6': return CrafterKeys::_6;
|
||||
case '7': return CrafterKeys::_7;
|
||||
case '8': return CrafterKeys::_8;
|
||||
case '9': return CrafterKeys::_9;
|
||||
|
||||
// Function keys
|
||||
case VK_F1: return CrafterKeys::F1;
|
||||
case VK_F2: return CrafterKeys::F2;
|
||||
case VK_F3: return CrafterKeys::F3;
|
||||
case VK_F4: return CrafterKeys::F4;
|
||||
case VK_F5: return CrafterKeys::F5;
|
||||
case VK_F6: return CrafterKeys::F6;
|
||||
case VK_F7: return CrafterKeys::F7;
|
||||
case VK_F8: return CrafterKeys::F8;
|
||||
case VK_F9: return CrafterKeys::F9;
|
||||
case VK_F10: return CrafterKeys::F10;
|
||||
case VK_F11: return CrafterKeys::F11;
|
||||
case VK_F12: return CrafterKeys::F12;
|
||||
|
||||
// Control keys
|
||||
case VK_ESCAPE: return CrafterKeys::Escape;
|
||||
case VK_TAB: return CrafterKeys::Tab;
|
||||
case VK_RETURN: return CrafterKeys::Enter;
|
||||
case VK_SPACE: return CrafterKeys::Space;
|
||||
case VK_BACK: return CrafterKeys::Backspace;
|
||||
case VK_DELETE: return CrafterKeys::Delete;
|
||||
case VK_INSERT: return CrafterKeys::Insert;
|
||||
case VK_HOME: return CrafterKeys::Home;
|
||||
case VK_END: return CrafterKeys::End;
|
||||
case VK_PRIOR: return CrafterKeys::PageUp;
|
||||
case VK_NEXT: return CrafterKeys::PageDown;
|
||||
case VK_CAPITAL: return CrafterKeys::CapsLock;
|
||||
case VK_NUMLOCK: return CrafterKeys::NumLock;
|
||||
case VK_SCROLL: return CrafterKeys::ScrollLock;
|
||||
|
||||
// Modifiers
|
||||
case VK_SHIFT: return CrafterKeys::LeftShift;
|
||||
case VK_LSHIFT: return CrafterKeys::LeftShift;
|
||||
case VK_RSHIFT: return CrafterKeys::RightShift;
|
||||
case VK_CONTROL: return CrafterKeys::LeftCtrl;
|
||||
case VK_LCONTROL: return CrafterKeys::LeftCtrl;
|
||||
case VK_RCONTROL: return CrafterKeys::RightCtrl;
|
||||
case VK_LMENU: return CrafterKeys::LeftAlt;
|
||||
case VK_RMENU: return CrafterKeys::RightAlt;
|
||||
case VK_LWIN: return CrafterKeys::LeftSuper;
|
||||
case VK_RWIN: return CrafterKeys::RightSuper;
|
||||
|
||||
// Arrows
|
||||
case VK_UP: return CrafterKeys::Up;
|
||||
case VK_DOWN: return CrafterKeys::Down;
|
||||
case VK_LEFT: return CrafterKeys::Left;
|
||||
case VK_RIGHT: return CrafterKeys::Right;
|
||||
|
||||
// Keypad
|
||||
case VK_NUMPAD0: return CrafterKeys::keypad_0;
|
||||
case VK_NUMPAD1: return CrafterKeys::keypad_1;
|
||||
case VK_NUMPAD2: return CrafterKeys::keypad_2;
|
||||
case VK_NUMPAD3: return CrafterKeys::keypad_3;
|
||||
case VK_NUMPAD4: return CrafterKeys::keypad_4;
|
||||
case VK_NUMPAD5: return CrafterKeys::keypad_5;
|
||||
case VK_NUMPAD6: return CrafterKeys::keypad_6;
|
||||
case VK_NUMPAD7: return CrafterKeys::keypad_7;
|
||||
case VK_NUMPAD8: return CrafterKeys::keypad_8;
|
||||
case VK_NUMPAD9: return CrafterKeys::keypad_9;
|
||||
case VK_SEPARATOR: return CrafterKeys::keypad_enter;
|
||||
case VK_ADD: return CrafterKeys::keypad_plus;
|
||||
case VK_SUBTRACT: return CrafterKeys::keypad_minus;
|
||||
case VK_MULTIPLY: return CrafterKeys::keypad_multiply;
|
||||
case VK_DIVIDE: return CrafterKeys::keypad_divide;
|
||||
case VK_DECIMAL: return CrafterKeys::keypad_decimal;
|
||||
|
||||
// Punctuation
|
||||
case VK_OEM_3: return CrafterKeys::grave; // `
|
||||
case VK_OEM_MINUS: return CrafterKeys::minus; // -
|
||||
case VK_OEM_PLUS: return CrafterKeys::equal; // =
|
||||
case VK_OEM_4: return CrafterKeys::bracket_left; // [
|
||||
case VK_OEM_6: return CrafterKeys::bracket_right; // ]
|
||||
case VK_OEM_5: return CrafterKeys::backslash; //
|
||||
case VK_OEM_1: return CrafterKeys::semicolon; // ;
|
||||
case VK_OEM_7: return CrafterKeys::quote; // '
|
||||
case VK_OEM_COMMA:return CrafterKeys::comma; // ,
|
||||
case VK_OEM_PERIOD:return CrafterKeys::period; // .
|
||||
case VK_OEM_2: return CrafterKeys::slash; // /
|
||||
|
||||
default: throw std::runtime_error(std::format("Unkown VK {}", vk));
|
||||
}
|
||||
// Extract the layout-independent raw key code from a WM_KEY* lParam. Bits
|
||||
// 16-23 hold the PS/2 set-1 scancode byte; bit 24 is the extended-key flag
|
||||
// (the 0xE0-prefixed variants — RightCtrl, RightAlt, the cursor cluster,
|
||||
// keypad Enter/Slash, the Windows keys). We pack the extended flag into bit
|
||||
// 8 of the returned KeyCode so it round-trips with the compile-time
|
||||
// `Key(CrafterKeys::...)` table in :Keys.
|
||||
static inline KeyCode KeyCodeFromLParam(LPARAM lParam) {
|
||||
return ((KeyCode)((lParam >> 16) & 0xFF))
|
||||
| (((lParam >> 24) & 1u) << 8);
|
||||
}
|
||||
|
||||
// Define a window class name
|
||||
|
|
@ -266,28 +151,36 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
|||
PostQuitMessage(0);
|
||||
break;
|
||||
}
|
||||
case WM_SIZE: {
|
||||
// SIZE_MINIMIZED reports (0, 0) — Resize() short-circuits, so
|
||||
// we just propagate the values directly. WM_SIZE fires
|
||||
// synchronously during a drag-resize loop; the StartSync
|
||||
// pump runs WndProc between frames, so the swapchain is
|
||||
// never touched mid-Render.
|
||||
if (window) {
|
||||
window->Resize(LOWORD(lParam), HIWORD(lParam));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case WM_KEYDOWN:
|
||||
case WM_SYSKEYDOWN: { // SYSKEYDOWN catches Alt combos, F10, etc.
|
||||
CrafterKeys crafterKey = vk_to_crafter_key(wParam);
|
||||
KeyCode code = KeyCodeFromLParam(lParam);
|
||||
bool isRepeat = (lParam & (1 << 30)) != 0;
|
||||
|
||||
if (isRepeat) {
|
||||
window->onKeyHold[(uint8_t)crafterKey].Invoke();
|
||||
window->onAnyKeyHold.Invoke(crafterKey);
|
||||
window->onRawKeyHold.Invoke(code);
|
||||
} else {
|
||||
window->heldkeys[(uint8_t)crafterKey] = true;
|
||||
window->onKeyDown[(uint8_t)crafterKey].Invoke();
|
||||
window->onAnyKeyDown.Invoke(crafterKey);
|
||||
window->heldKeys.insert(code);
|
||||
window->onRawKeyDown.Invoke(code);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case WM_KEYUP:
|
||||
case WM_SYSKEYUP: {
|
||||
CrafterKeys crafterKey = vk_to_crafter_key(wParam);
|
||||
window->heldkeys[(uint8_t)crafterKey] = false;
|
||||
window->onKeyUp[(uint8_t)crafterKey].Invoke();
|
||||
window->onAnyKeyUp.Invoke(crafterKey);
|
||||
KeyCode code = KeyCodeFromLParam(lParam);
|
||||
window->heldKeys.erase(code);
|
||||
window->onRawKeyUp.Invoke(code);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -510,6 +403,93 @@ Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height
|
|||
currentMousePos = {0,0};
|
||||
}
|
||||
|
||||
void Window::Resize(std::uint32_t newWidth, std::uint32_t newHeight) {
|
||||
// Skip degenerate resizes. Win32 minimised windows give (0, 0); Wayland
|
||||
// sometimes echoes the current size in a configure.
|
||||
if (newWidth == 0 || newHeight == 0) return;
|
||||
if (newWidth == width && newHeight == height) return;
|
||||
// Win32 fires WM_SIZE synchronously inside CreateWindowEx (before the
|
||||
// constructor's CreateSwapchain). Defer the first resize to that
|
||||
// CreateSwapchain call instead of trying to recreate a non-existent
|
||||
// swapchain.
|
||||
if (swapChain == VK_NULL_HANDLE) {
|
||||
width = newWidth;
|
||||
height = newHeight;
|
||||
return;
|
||||
}
|
||||
|
||||
width = newWidth;
|
||||
height = newHeight;
|
||||
|
||||
// Caller (configure handler / WM_SIZE) runs between frames, but be
|
||||
// defensive: ensure no in-flight commands reference the old swapchain.
|
||||
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
||||
|
||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
||||
if (wpViewport) {
|
||||
wp_viewport_set_destination(wpViewport,
|
||||
static_cast<int>(std::ceil(width / scale)),
|
||||
static_cast<int>(std::ceil(height / scale)));
|
||||
}
|
||||
#endif
|
||||
|
||||
RecreateSwapchainAndImages();
|
||||
onResize.Invoke();
|
||||
}
|
||||
|
||||
void Window::RecreateSwapchainAndImages() {
|
||||
CreateSwapchain();
|
||||
|
||||
// CreateSwapchain leaves new swapchain images in VK_IMAGE_LAYOUT_UNDEFINED.
|
||||
// Render() barriers from PRESENT_SRC_KHR, so transition them now to
|
||||
// match. Mirrors the StartInit logic, on a one-shot command buffer.
|
||||
{
|
||||
VkCommandBufferAllocateInfo cba{
|
||||
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
|
||||
.commandPool = Device::commandPool,
|
||||
.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
|
||||
.commandBufferCount = 1,
|
||||
};
|
||||
VkCommandBuffer cmd = VK_NULL_HANDLE;
|
||||
Device::CheckVkResult(vkAllocateCommandBuffers(Device::device, &cba, &cmd));
|
||||
|
||||
VkCommandBufferBeginInfo cbi{
|
||||
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
|
||||
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
|
||||
};
|
||||
Device::CheckVkResult(vkBeginCommandBuffer(cmd, &cbi));
|
||||
|
||||
VkImageSubresourceRange range{
|
||||
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
|
||||
.baseMipLevel = 0,
|
||||
.levelCount = VK_REMAINING_MIP_LEVELS,
|
||||
.baseArrayLayer = 0,
|
||||
.layerCount = VK_REMAINING_ARRAY_LAYERS,
|
||||
};
|
||||
for (std::uint32_t i = 0; i < numFrames; i++) {
|
||||
image_layout_transition(cmd,
|
||||
images[i],
|
||||
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
|
||||
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
|
||||
0, 0,
|
||||
VK_IMAGE_LAYOUT_UNDEFINED,
|
||||
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
|
||||
range);
|
||||
}
|
||||
|
||||
Device::CheckVkResult(vkEndCommandBuffer(cmd));
|
||||
|
||||
VkSubmitInfo si{
|
||||
.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
|
||||
.commandBufferCount = 1,
|
||||
.pCommandBuffers = &cmd,
|
||||
};
|
||||
Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &si, VK_NULL_HANDLE));
|
||||
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
||||
vkFreeCommandBuffers(Device::device, Device::commandPool, 1, &cmd);
|
||||
}
|
||||
}
|
||||
|
||||
void Window::SetTitle(const std::string_view title) {
|
||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
||||
xdg_toplevel_set_title(xdgToplevel, title.data());
|
||||
|
|
@ -630,6 +610,7 @@ void Window::SetDefaultCursor() {
|
|||
void Window::StartSync() {
|
||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
||||
while (open && wl_display_dispatch(Device::display) != -1) {
|
||||
Gamepad::Tick();
|
||||
onBeforeUpdate.Invoke();
|
||||
}
|
||||
#endif
|
||||
|
|
@ -640,6 +621,7 @@ void Window::StartSync() {
|
|||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
Gamepad::Tick();
|
||||
onBeforeUpdate.Invoke();
|
||||
if(updating) {
|
||||
Update();
|
||||
|
|
@ -697,8 +679,23 @@ void Window::Update() {
|
|||
}
|
||||
|
||||
void Window::Render() {
|
||||
// Acquire the next image from the swap chain
|
||||
Device::CheckVkResult(vkAcquireNextImageKHR(Device::device, swapChain, UINT64_MAX, semaphores.presentComplete, (VkFence)nullptr, ¤tBuffer));
|
||||
// Acquire the next image from the swap chain. If the surface has
|
||||
// changed size out from under us (compositor/Win32 resize delivered
|
||||
// between Render calls), recreate and retry once.
|
||||
{
|
||||
VkResult acquire = vkAcquireNextImageKHR(Device::device, swapChain, UINT64_MAX,
|
||||
semaphores.presentComplete, (VkFence)nullptr, ¤tBuffer);
|
||||
if (acquire == VK_ERROR_OUT_OF_DATE_KHR) {
|
||||
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
||||
RecreateSwapchainAndImages();
|
||||
onResize.Invoke();
|
||||
acquire = vkAcquireNextImageKHR(Device::device, swapChain, UINT64_MAX,
|
||||
semaphores.presentComplete, (VkFence)nullptr, ¤tBuffer);
|
||||
}
|
||||
if (acquire != VK_SUBOPTIMAL_KHR) {
|
||||
Device::CheckVkResult(acquire);
|
||||
}
|
||||
}
|
||||
submitInfo.commandBufferCount = 1;
|
||||
submitInfo.pCommandBuffers = &drawCmdBuffers[currentBuffer];
|
||||
|
||||
|
|
@ -822,8 +819,13 @@ void Window::Render() {
|
|||
}
|
||||
|
||||
VkResult result = vkQueuePresentKHR(Device::queue, &presentInfo);
|
||||
if(result == VK_SUBOPTIMAL_KHR) {
|
||||
CreateSwapchain();
|
||||
if (result == VK_SUBOPTIMAL_KHR || result == VK_ERROR_OUT_OF_DATE_KHR) {
|
||||
// Surface size changed mid-present. Drain the queue, rebuild the
|
||||
// swapchain, and let dependents (descriptors holding old image
|
||||
// handles) re-bind via onResize before the next frame.
|
||||
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
||||
RecreateSwapchainAndImages();
|
||||
onResize.Invoke();
|
||||
} else {
|
||||
Device::CheckVkResult(result);
|
||||
}
|
||||
|
|
@ -1067,8 +1069,13 @@ void Window::wl_surface_frame_done(void* data, struct wl_callback *cb, uint32_t
|
|||
}
|
||||
|
||||
|
||||
void Window::xdg_toplevel_configure(void*, xdg_toplevel*, std::int32_t, std::int32_t, wl_array*){
|
||||
|
||||
void Window::xdg_toplevel_configure(void* data, xdg_toplevel*, std::int32_t width, std::int32_t height, wl_array*){
|
||||
// xdg-shell batches state — width/height are pending until the matching
|
||||
// xdg_surface.configure arrives. Width/height are in surface-local
|
||||
// (logical DP) units; (0, 0) means "compositor has no preference".
|
||||
Window* window = reinterpret_cast<Window*>(data);
|
||||
window->pendingLogicalWidth = width;
|
||||
window->pendingLogicalHeight = height;
|
||||
}
|
||||
|
||||
void Window::xdg_toplevel_handle_close(void* data, xdg_toplevel*) {
|
||||
|
|
@ -1083,9 +1090,20 @@ void Window::xdg_surface_handle_configure(void* data, xdg_surface* xdg_surface,
|
|||
xdg_surface_ack_configure(xdg_surface, serial);
|
||||
|
||||
if (window->configured) {
|
||||
// If this isn't the first configure event we've received, we already
|
||||
// have a buffer attached, so no need to do anything. Commit the
|
||||
// surface to apply the configure acknowledgement.
|
||||
// Subsequent configure: if the toplevel asked for a new size
|
||||
// (non-zero, different from current), drive the resize end-to-end.
|
||||
// (0, 0) means "compositor has no preference, keep current size".
|
||||
// The swapchain may not exist yet on the very first frame between
|
||||
// the constructor's wait loop and CreateSwapchain — the Resize
|
||||
// guard against equal sizes already covers that path.
|
||||
if (window->pendingLogicalWidth > 0 && window->pendingLogicalHeight > 0 &&
|
||||
window->swapChain != VK_NULL_HANDLE) {
|
||||
std::uint32_t newWidth = static_cast<std::uint32_t>(
|
||||
std::ceil(window->pendingLogicalWidth * window->scale));
|
||||
std::uint32_t newHeight = static_cast<std::uint32_t>(
|
||||
std::ceil(window->pendingLogicalHeight * window->scale));
|
||||
window->Resize(newWidth, newHeight);
|
||||
}
|
||||
wl_surface_commit(window->surface);
|
||||
}
|
||||
|
||||
|
|
|
|||
42
interfaces/Crafter.Graphics-Clipboard.cppm
Normal file
42
interfaces/Crafter.Graphics-Clipboard.cppm
Normal 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);
|
||||
}
|
||||
133
interfaces/Crafter.Graphics-Decompress.cppm
Normal file
133
interfaces/Crafter.Graphics-Decompress.cppm
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
99
interfaces/Crafter.Graphics-Gamepad.cppm
Normal file
99
interfaces/Crafter.Graphics-Gamepad.cppm
Normal 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);
|
||||
}
|
||||
|
|
@ -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, ®ion);
|
||||
|
||||
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);
|
||||
|
|
|
|||
180
interfaces/Crafter.Graphics-Input.cppm
Normal file
180
interfaces/Crafter.Graphics-Input.cppm
Normal 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);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
331
interfaces/Crafter.Graphics-Keys.cppm
Normal file
331
interfaces/Crafter.Graphics-Keys.cppm
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -43,3 +46,5 @@ export import :ComputeShader;
|
|||
export import :UI;
|
||||
export import :UIComponents;
|
||||
export import :InputField;
|
||||
export import :Clipboard;
|
||||
export import :Decompress;
|
||||
55
project.cpp
55
project.cpp
|
|
@ -5,18 +5,35 @@ using namespace Crafter;
|
|||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
||||
std::vector<std::string> depArgs(args.begin(), args.end());
|
||||
Configuration* event = GitProject({
|
||||
.source = { .url = "https://forgejo.catcrafts.net/Catcrafts/Crafter.Event.git" },
|
||||
|
||||
// --local resolves Crafter.* deps from sibling working trees instead of
|
||||
// fetching them from forgejo. Use during cross-repo development so edits
|
||||
// in ../Crafter.Asset are picked up without commit-and-pull. Add to
|
||||
// depArgs too so transitive deps inherit the same mode.
|
||||
bool useLocal = false;
|
||||
for (std::string_view a : args) {
|
||||
if (a == "--local") { useLocal = true; break; }
|
||||
}
|
||||
if (useLocal && std::find(depArgs.begin(), depArgs.end(), std::string("--local")) == depArgs.end()) {
|
||||
depArgs.push_back("--local");
|
||||
}
|
||||
|
||||
auto resolveDep = [&](std::string_view name, std::string_view gitUrl) -> Configuration* {
|
||||
if (useLocal) {
|
||||
return LocalProject({
|
||||
.projectFile = fs::path("../") / name / "project.cpp",
|
||||
.args = depArgs,
|
||||
});
|
||||
Configuration* math = GitProject({
|
||||
.source = { .url = "https://forgejo.catcrafts.net/Catcrafts/Crafter.Math.git" },
|
||||
.args = depArgs,
|
||||
});
|
||||
Configuration* asset = GitProject({
|
||||
.source = { .url = "https://forgejo.catcrafts.net/Catcrafts/Crafter.Asset.git" },
|
||||
}
|
||||
return GitProject({
|
||||
.source = { .url = std::string(gitUrl) },
|
||||
.args = depArgs,
|
||||
});
|
||||
};
|
||||
|
||||
Configuration* event = resolveDep("Crafter.Event", "https://forgejo.catcrafts.net/Catcrafts/Crafter.Event.git");
|
||||
Configuration* math = resolveDep("Crafter.Math", "https://forgejo.catcrafts.net/Catcrafts/Crafter.Math.git");
|
||||
Configuration* asset = resolveDep("Crafter.Asset", "https://forgejo.catcrafts.net/Catcrafts/Crafter.Asset.git");
|
||||
|
||||
Configuration cfg;
|
||||
cfg.path = "./";
|
||||
|
|
@ -37,10 +54,20 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
cfg.linkFlags.push_back("-lkernel32");
|
||||
cfg.linkFlags.push_back("-luser32");
|
||||
cfg.linkFlags.push_back("-lgdi32");
|
||||
// Windows.Gaming.Input (WGI) needs the WinRT activation runtime
|
||||
// and combase for HSTRING / RoGetActivationFactory.
|
||||
cfg.linkFlags.push_back("-lruntimeobject");
|
||||
cfg.linkFlags.push_back("-lcombase");
|
||||
} else {
|
||||
cfg.defines.push_back({"CRAFTER_GRAPHICS_WINDOW_WAYLAND", ""});
|
||||
cfg.linkFlags.push_back("-lwayland-client");
|
||||
cfg.linkFlags.push_back("-lxkbcommon");
|
||||
// Gamepad: libudev for hot-plug + device enumeration; libevdev
|
||||
// for event parsing + axis calibration. libevdev ships its headers
|
||||
// under a versioned dir (libevdev-1.0/) so the -I is mandatory.
|
||||
cfg.linkFlags.push_back("-ludev");
|
||||
cfg.linkFlags.push_back("-levdev");
|
||||
cfg.compileFlags.push_back("-I/usr/include/libevdev-1.0");
|
||||
cfg.cFiles.push_back("lib/xdg-shell-protocol");
|
||||
cfg.cFiles.push_back("lib/wayland-xdg-decoration-unstable-v1-client-protocol");
|
||||
cfg.cFiles.push_back("lib/fractional-scale-v1");
|
||||
|
|
@ -63,17 +90,22 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
|
||||
if (opts.Has("--timing")) cfg.defines.push_back({"CRAFTER_TIMING", ""});
|
||||
|
||||
std::array<fs::path, 24> ifaces = {
|
||||
std::array<fs::path, 29> ifaces = {
|
||||
"interfaces/Crafter.Graphics",
|
||||
"interfaces/Crafter.Graphics-Animation",
|
||||
"interfaces/Crafter.Graphics-Clipboard",
|
||||
"interfaces/Crafter.Graphics-ComputeShader",
|
||||
"interfaces/Crafter.Graphics-Decompress",
|
||||
"interfaces/Crafter.Graphics-DescriptorHeapVulkan",
|
||||
"interfaces/Crafter.Graphics-Device",
|
||||
"interfaces/Crafter.Graphics-Font",
|
||||
"interfaces/Crafter.Graphics-FontAtlas",
|
||||
"interfaces/Crafter.Graphics-ForwardDeclarations",
|
||||
"interfaces/Crafter.Graphics-Gamepad",
|
||||
"interfaces/Crafter.Graphics-ImageVulkan",
|
||||
"interfaces/Crafter.Graphics-Input",
|
||||
"interfaces/Crafter.Graphics-InputField",
|
||||
"interfaces/Crafter.Graphics-Keys",
|
||||
"interfaces/Crafter.Graphics-Mesh",
|
||||
"interfaces/Crafter.Graphics-PipelineRTVulkan",
|
||||
"interfaces/Crafter.Graphics-RenderingElement3D",
|
||||
|
|
@ -89,11 +121,14 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
"interfaces/Crafter.Graphics-VulkanTransition",
|
||||
"interfaces/Crafter.Graphics-Window",
|
||||
};
|
||||
std::array<fs::path, 10> impls = {
|
||||
std::array<fs::path, 13> impls = {
|
||||
"implementations/Crafter.Graphics-Clipboard",
|
||||
"implementations/Crafter.Graphics-ComputeShader",
|
||||
"implementations/Crafter.Graphics-Device",
|
||||
"implementations/Crafter.Graphics-Font",
|
||||
"implementations/Crafter.Graphics-FontAtlas",
|
||||
"implementations/Crafter.Graphics-Gamepad",
|
||||
"implementations/Crafter.Graphics-Input",
|
||||
"implementations/Crafter.Graphics-InputField",
|
||||
"implementations/Crafter.Graphics-Mesh",
|
||||
"implementations/Crafter.Graphics-RenderingElement3D",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue