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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue