Compare commits
34 changed files with 108 additions and 9812 deletions
23
README.md
23
README.md
|
|
@ -41,23 +41,15 @@ shaded through an intersection shader with an any-hit cut-out.
|
||||||
> reads no descriptor), and `RTPass` supplies the address as push data.
|
> reads no descriptor), and `RTPass` supplies the address as push data.
|
||||||
> Shaders and example code are unchanged, and it's a single fenced block
|
> Shaders and example code are unchanged, and it's a single fenced block
|
||||||
> gated on `Device::workaroundDescriptorHeapAS`, removable once a fixed
|
> gated on `Device::workaroundDescriptorHeapAS`, removable once a fixed
|
||||||
> driver ships. **Still present on `615.78.08`**: with the rewrite off,
|
> driver ships. See
|
||||||
> `traceRayEXT` still loses the device and an inline ray query renders
|
|
||||||
> nothing. `tests/HeapASRead` covers the engine path; run
|
|
||||||
> `HeapASRead --no-workaround` to check a newer driver (it tries both paths
|
|
||||||
> with the rewrite off). Any app runs with the rewrite off while
|
|
||||||
> `CRAFTER_GRAPHICS_NO_AS_WORKAROUND` is set, to any value. See
|
|
||||||
> [examples/VulkanTriangle/README.md](examples/VulkanTriangle/README.md)
|
> [examples/VulkanTriangle/README.md](examples/VulkanTriangle/README.md)
|
||||||
> for the full investigation. WebGPU RT is unaffected.
|
> for the full investigation. WebGPU RT is unaffected.
|
||||||
|
|
||||||
> **Native RT limitation (fixed in NVIDIA `615.78.08`) — dynamic
|
> **Native RT limitation — dynamic `descriptor_heap` indexing in hit
|
||||||
> `descriptor_heap` indexing in hit shaders:** on NVIDIA `610.43.02`, indexing
|
> shaders:** on the same NVIDIA driver, indexing a `descriptor_heap`
|
||||||
> a `descriptor_heap` array with a **runtime (non-constant)** index inside a
|
> array with a **runtime (non-constant)** index inside a ray-tracing
|
||||||
> ray-tracing **hit** shader also device-losts (`VK_ERROR_DEVICE_LOST`), for
|
> **hit** shader also device-losts (`VK_ERROR_DEVICE_LOST`), for plain
|
||||||
> plain SSBO **and** sampled-image descriptors. On `615.78.08` it works:
|
> SSBO **and** sampled-image descriptors. A **constant / spec-constant**
|
||||||
> `tests/HeapDynamicIndexHit` picks a buffer and a texture by a runtime index
|
|
||||||
> in a closest-hit, including different descriptors in neighbouring lanes of
|
|
||||||
> a wave. The rest of this note applies to the older drivers. A **constant / spec-constant**
|
|
||||||
> index is fine (that's why [Sponza](examples/Sponza/README.md)'s
|
> index is fine (that's why [Sponza](examples/Sponza/README.md)'s
|
||||||
> closest-hit reads `albedo[albedoSlot]` through a spec constant), and
|
> closest-hit reads `albedo[albedoSlot]` through a spec constant), and
|
||||||
> the identical dynamic pattern works in fragment shaders (the UI
|
> the identical dynamic pattern works in fragment shaders (the UI
|
||||||
|
|
@ -137,8 +129,7 @@ shaded through an intersection shader with an any-hit cut-out.
|
||||||
per-vertex attribs) into global mesh heaps and builds the TLAS in a
|
per-vertex attribs) into global mesh heaps and builds the TLAS in a
|
||||||
library compute pass.
|
library compute pass.
|
||||||
- **Clipboard / Input / Gamepad / Router / Dom** — input plumbing.
|
- **Clipboard / Input / Gamepad / Router / Dom** — input plumbing.
|
||||||
Gamepad uses libudev+libevdev on Linux (libevdev is vendored in
|
Gamepad uses libudev+libevdev on Linux and WGI on Windows; the DOM
|
||||||
`lib/libevdev`) and WGI on Windows; the DOM
|
|
||||||
backend exposes the host page DOM (`Dom::HtmlElement`) and a router
|
backend exposes the host page DOM (`Dom::HtmlElement`) and a router
|
||||||
for hash-routed wasm apps.
|
for hash-routed wasm apps.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,6 @@ below.
|
||||||
|
|
||||||
## Native RT limitation: dynamic `descriptor_heap` indexing in hit shaders
|
## Native RT limitation: dynamic `descriptor_heap` indexing in hit shaders
|
||||||
|
|
||||||
Fixed in NVIDIA `615.78.08`: `tests/HeapDynamicIndexHit` selects buffers and
|
|
||||||
textures with a runtime index in a closest-hit, including different
|
|
||||||
descriptors in neighbouring lanes of a wave. This example still uses the
|
|
||||||
spec-constant pattern below, which also runs on the older drivers.
|
|
||||||
|
|
||||||
On NVIDIA driver `610.43.02` (Vulkan 1.4), indexing a
|
On NVIDIA driver `610.43.02` (Vulkan 1.4), indexing a
|
||||||
`layout(descriptor_heap)` array with a **runtime (non-constant)** index
|
`layout(descriptor_heap)` array with a **runtime (non-constant)** index
|
||||||
inside a ray-tracing **hit** shader aborts the device with
|
inside a ray-tracing **hit** shader aborts the device with
|
||||||
|
|
@ -127,6 +122,8 @@ This is precisely what the WebGPU path already does — bucketed texture
|
||||||
arrays plus a single geometry buffer — so it is a proven, cross-backend
|
arrays plus a single geometry buffer — so it is a proven, cross-backend
|
||||||
pattern, and it sidesteps the NVIDIA RT fault on the native path.
|
pattern, and it sidesteps the NVIDIA RT fault on the native path.
|
||||||
|
|
||||||
|
Remove this section once a fixed NVIDIA driver ships and dynamic
|
||||||
|
`descriptor_heap` indexing in hit shaders stops faulting.
|
||||||
|
|
||||||
## Asset fetch
|
## Asset fetch
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ exits.
|
||||||
`Window::Render()`, reporting per-frame wall-clock stats and throughput.
|
`Window::Render()`, reporting per-frame wall-clock stats and throughput.
|
||||||
|
|
||||||
Unlike the interactive Sponza example — which is single-material on native
|
Unlike the interactive Sponza example — which is single-material on native
|
||||||
because of the hit-shader dynamic-`descriptor_heap`-index driver fault, fixed
|
because of the hit-shader dynamic-`descriptor_heap`-index driver fault
|
||||||
in NVIDIA `615.78.08` (see `examples/Sponza/README.md`) — SponzaBench's closest-hit shades from
|
(see `examples/Sponza/README.md`) — SponzaBench's closest-hit shades from
|
||||||
barycentric coordinates and samples **no** texture, so it can build the
|
barycentric coordinates and samples **no** texture, so it can build the
|
||||||
**full multi-mesh atrium** (25 mesh groups, ~262 k triangles) as one
|
**full multi-mesh atrium** (25 mesh groups, ~262 k triangles) as one
|
||||||
multi-instance TLAS. The albedo is still decompressed and uploaded during
|
multi-instance TLAS. The albedo is still decompressed and uploaded during
|
||||||
|
|
|
||||||
|
|
@ -58,17 +58,6 @@ every other driver the workaround is inert. It's gated on
|
||||||
`interfaces/Crafter.Graphics-ShaderVulkan.cppm` so it can be deleted wholesale
|
`interfaces/Crafter.Graphics-ShaderVulkan.cppm` so it can be deleted wholesale
|
||||||
once a fixed NVIDIA driver ships.
|
once a fixed NVIDIA driver ships.
|
||||||
|
|
||||||
### Status on later drivers
|
|
||||||
|
|
||||||
On `615.78.08` the fault is still there. With the rewrite off
|
|
||||||
(`CRAFTER_GRAPHICS_NO_AS_WORKAROUND` set), `traceRayEXT` still loses the device,
|
|
||||||
and an inline ray query that reads the TLAS from the heap no longer faults but
|
|
||||||
renders nothing. Mesa's llvmpipe now advertises `VK_EXT_descriptor_heap` too
|
|
||||||
and renders the plain heap read correctly, which is a second implementation
|
|
||||||
backing the conclusion below. `tests/HeapASRead` renders this triangle through
|
|
||||||
both paths and checks the pixels; `HeapASRead --no-workaround` is the quick
|
|
||||||
check for a new driver, reporting each path on its own.
|
|
||||||
|
|
||||||
### The underlying fault (#7)
|
### The underlying fault (#7)
|
||||||
|
|
||||||
The fault was traced to the **acceleration-structure read through
|
The fault was traced to the **acceleration-structure read through
|
||||||
|
|
@ -94,10 +83,9 @@ The fault was traced to the **acceleration-structure read through
|
||||||
- The fault reproduces even with the AS descriptor written at heap byte 0
|
- The fault reproduces even with the AS descriptor written at heap byte 0
|
||||||
and read at shader index 0 (no descriptor offset/stride ambiguity), and
|
and read at shader index 0 (no descriptor offset/stride ambiguity), and
|
||||||
is unaffected by the `pAddressRange` size.
|
is unaffected by the `pAddressRange` size.
|
||||||
- `VK_EXT_descriptor_heap` is brand new; at the time NVIDIA was the only
|
- `VK_EXT_descriptor_heap` is brand new; on this machine NVIDIA is the only
|
||||||
implementation on this machine that advertised it (llvmpipe did not), so
|
implementation that advertises it (llvmpipe does not), so there is no
|
||||||
there was no second conformant implementation to cross-check against.
|
second conformant implementation to cross-check against.
|
||||||
Mesa's llvmpipe has it now; see "Status on later drivers" above.
|
|
||||||
|
|
||||||
**Conclusion:** this is a driver-side fault in NVIDIA's
|
**Conclusion:** this is a driver-side fault in NVIDIA's
|
||||||
`VK_EXT_descriptor_heap` acceleration-structure path, not an engine bug, and
|
`VK_EXT_descriptor_heap` acceleration-structure path, not an engine bug, and
|
||||||
|
|
|
||||||
|
|
@ -1061,12 +1061,9 @@ void Device::Initialize() {
|
||||||
// the heap, so it is off whenever the heap is: otherwise a forced-fallback
|
// the heap, so it is off whenever the heap is: otherwise a forced-fallback
|
||||||
// run on NVIDIA would still rewrite SPIR-V, enable shaderInt64 and push the
|
// run on NVIDIA would still rewrite SPIR-V, enable shaderInt64 and push the
|
||||||
// TLAS address through vkCmdPushDataEXT, none of which a real heap-less
|
// TLAS address through vkCmdPushDataEXT, none of which a real heap-less
|
||||||
// driver does. Still needed on 615.78.08. CRAFTER_GRAPHICS_NO_AS_WORKAROUND,
|
// driver does.
|
||||||
// set to any value like the other CRAFTER_GRAPHICS_* switches, turns it off,
|
|
||||||
// which is how a new driver gets checked (tests/HeapASRead --no-workaround).
|
|
||||||
workaroundDescriptorHeapAS = descriptorHeapSupported
|
workaroundDescriptorHeapAS = descriptorHeapSupported
|
||||||
&& driverProperties.driverID == VK_DRIVER_ID_NVIDIA_PROPRIETARY
|
&& driverProperties.driverID == VK_DRIVER_ID_NVIDIA_PROPRIETARY;
|
||||||
&& std::getenv("CRAFTER_GRAPHICS_NO_AS_WORKAROUND") == nullptr;
|
|
||||||
|
|
||||||
// Sanity-gate: GDeflate 1.0 must actually be in the supported method set.
|
// Sanity-gate: GDeflate 1.0 must actually be in the supported method set.
|
||||||
if (memoryDecompressionSupported &&
|
if (memoryDecompressionSupported &&
|
||||||
|
|
|
||||||
|
|
@ -119,15 +119,10 @@ void RenderingElement3D::BuildTLAS(VkCommandBuffer cmd, std::uint32_t index, RTB
|
||||||
// (no-op when the chosen type is coherent — the previous behaviour),
|
// (no-op when the chosen type is coherent — the previous behaviour),
|
||||||
// and the queue submit carries the host-write -> shader-read ordering as
|
// and the queue submit carries the host-write -> shader-read ordering as
|
||||||
// it always has.
|
// it always has.
|
||||||
//
|
|
||||||
// At least one slot each: a TLAS built before anything is in the scene
|
|
||||||
// has zero instances, and a zero-size buffer is invalid (NVIDIA fails
|
|
||||||
// the allocation). The empty build reads no instance anyway.
|
|
||||||
if (tlas.instanceBuffer.buffer == VK_NULL_HANDLE
|
if (tlas.instanceBuffer.buffer == VK_NULL_HANDLE
|
||||||
|| primitiveCount > tlas.instanceBuffer.size / sizeof(VkAccelerationStructureInstanceKHR)) {
|
|| primitiveCount > tlas.instanceBuffer.size / sizeof(VkAccelerationStructureInstanceKHR)) {
|
||||||
const std::uint32_t capacity = std::max(primitiveCount, 1u);
|
tlas.instanceBuffer.Resize(VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, primitiveCount, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||||||
tlas.instanceBuffer.Resize(VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, capacity, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
tlas.metadataBuffer.Resize(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, primitiveCount, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||||||
tlas.metadataBuffer.Resize(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, capacity, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A topology change reshuffles which element occupies each slot (a count
|
// A topology change reshuffles which element occupies each slot (a count
|
||||||
|
|
@ -255,9 +250,7 @@ void RenderingElement3D::BuildTLAS(VkCommandBuffer cmd, std::uint32_t index, RTB
|
||||||
|
|
||||||
// Scratch buffer must hold at least max(buildScratchSize, updateScratchSize).
|
// Scratch buffer must hold at least max(buildScratchSize, updateScratchSize).
|
||||||
// Sizing for buildScratchSize covers both — refit is always smaller.
|
// Sizing for buildScratchSize covers both — refit is always smaller.
|
||||||
// An empty TLAS may need no scratch at all, but the buffer can't be
|
tlas.scratchBuffer.Resize(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, tlasBuildSizes.buildScratchSize);
|
||||||
// zero-size.
|
|
||||||
tlas.scratchBuffer.Resize(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, std::max<VkDeviceSize>(tlasBuildSizes.buildScratchSize, 1));
|
|
||||||
tlas.buffer.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, tlasBuildSizes.accelerationStructureSize);
|
tlas.buffer.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, tlasBuildSizes.accelerationStructureSize);
|
||||||
|
|
||||||
// Destroy the previous AS handle before creating a new one — the
|
// Destroy the previous AS handle before creating a new one — the
|
||||||
|
|
@ -302,6 +295,13 @@ void RenderingElement3D::BuildTLAS(VkCommandBuffer cmd, std::uint32_t index, RTB
|
||||||
VkAccelerationStructureBuildRangeInfoKHR* tlasRangeInfoPP = &tlasRangeInfo;
|
VkAccelerationStructureBuildRangeInfoKHR* tlasRangeInfoPP = &tlasRangeInfo;
|
||||||
Device::vkCmdBuildAccelerationStructuresKHR(cmd, 1, &tlasBuildGeometryInfo, &tlasRangeInfoPP);
|
Device::vkCmdBuildAccelerationStructuresKHR(cmd, 1, &tlasBuildGeometryInfo, &tlasRangeInfoPP);
|
||||||
|
|
||||||
const TlasReadBarrier readBarrier = BuildTlasReadBarrier();
|
vkCmdPipelineBarrier(
|
||||||
vkCmdPipelineBarrier(cmd, readBarrier.srcStage, readBarrier.dstStage, 0, 1, &readBarrier.memory, 0, nullptr, 0, nullptr);
|
cmd,
|
||||||
|
VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR,
|
||||||
|
VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
|
||||||
|
0,
|
||||||
|
0, nullptr,
|
||||||
|
0, nullptr,
|
||||||
|
0, nullptr
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -281,11 +281,8 @@ export namespace Crafter {
|
||||||
// VK_ERROR_DEVICE_LOST (a brand-new-extension driver fault, verified
|
// VK_ERROR_DEVICE_LOST (a brand-new-extension driver fault, verified
|
||||||
// engine-clean in #7). When set, VulkanShader rewrites heap AS reads
|
// engine-clean in #7). When set, VulkanShader rewrites heap AS reads
|
||||||
// into a TLAS-device-address + OpConvertUToAccelerationStructureKHR
|
// into a TLAS-device-address + OpConvertUToAccelerationStructureKHR
|
||||||
// path and RTPass pushes the active TLAS address as push data. Still
|
// path and RTPass pushes the active TLAS address as push data. Delete
|
||||||
// needed on 615.78.08. CRAFTER_GRAPHICS_NO_AS_WORKAROUND, set to any
|
// this flag and everything keyed on it once a fixed driver ships.
|
||||||
// value, turns it off to check a newer driver (tests/HeapASRead
|
|
||||||
// --no-workaround). Delete this flag and everything keyed on it once a
|
|
||||||
// fixed driver ships.
|
|
||||||
inline static bool workaroundDescriptorHeapAS = false;
|
inline static bool workaroundDescriptorHeapAS = false;
|
||||||
|
|
||||||
// Count of ERROR-severity validation messages seen by the debug
|
// Count of ERROR-severity validation messages seen by the debug
|
||||||
|
|
|
||||||
|
|
@ -115,14 +115,7 @@ export namespace Crafter {
|
||||||
inline static std::uint64_t hostDataVersionCounter = 0;
|
inline static std::uint64_t hostDataVersionCounter = 0;
|
||||||
|
|
||||||
static std::vector<RenderingElement3D*> elements;
|
static std::vector<RenderingElement3D*> elements;
|
||||||
// Never destroyed, like Device::device. As a plain static its
|
inline static TlasWithBuffer tlases[Window::numFrames];
|
||||||
// destructor would run at process exit after the Vulkan driver and
|
|
||||||
// layers, which are loaded during Device::Initialize (after this was
|
|
||||||
// constructed), have torn down their own state, and destroying the
|
|
||||||
// buffers would call into that freed state: the validation layer
|
|
||||||
// segfaults on it. Exit reclaims them instead.
|
|
||||||
inline static std::array<TlasWithBuffer, Window::numFrames>& tlases =
|
|
||||||
*new std::array<TlasWithBuffer, Window::numFrames>();
|
|
||||||
// Build (or in-place refit) the TLAS for frame `index`. `preference`
|
// Build (or in-place refit) the TLAS for frame `index`. `preference`
|
||||||
// maps onto the FAST_TRACE/FAST_BUILD bits exactly as Mesh::Build
|
// maps onto the FAST_TRACE/FAST_BUILD bits exactly as Mesh::Build
|
||||||
// does for a BLAS: FastTrace (the default) spends more build time for
|
// does for a BLAS: FastTrace (the default) spends more build time for
|
||||||
|
|
@ -141,29 +134,6 @@ export namespace Crafter {
|
||||||
static void Add(RenderingElement3D* e);
|
static void Add(RenderingElement3D* e);
|
||||||
static void Remove(RenderingElement3D* e);
|
static void Remove(RenderingElement3D* e);
|
||||||
};
|
};
|
||||||
|
|
||||||
// The dependency BuildTLAS records after its build, ordering it before
|
|
||||||
// the shaders that read the TLAS later in the same command buffer:
|
|
||||||
// traceRayEXT in a ray-tracing pass, rayQueryEXT in a compute pass. It
|
|
||||||
// has to carry the acceleration-structure memory dependency; ordering
|
|
||||||
// execution alone never makes the build's writes visible to those reads.
|
|
||||||
struct TlasReadBarrier {
|
|
||||||
VkPipelineStageFlags srcStage;
|
|
||||||
VkPipelineStageFlags dstStage;
|
|
||||||
VkMemoryBarrier memory;
|
|
||||||
};
|
|
||||||
|
|
||||||
inline TlasReadBarrier BuildTlasReadBarrier() {
|
|
||||||
return {
|
|
||||||
.srcStage = VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR,
|
|
||||||
.dstStage = VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
|
|
||||||
.memory = {
|
|
||||||
.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER,
|
|
||||||
.srcAccessMask = VK_ACCESS_ACCELERATION_STRUCTURE_WRITE_BIT_KHR,
|
|
||||||
.dstAccessMask = VK_ACCESS_ACCELERATION_STRUCTURE_READ_BIT_KHR,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
Copyright © 2013 Red Hat, Inc.
|
|
||||||
Copyright © 2013 David Herrmann <dh.herrmann@gmail.com>
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to
|
|
||||||
deal in the Software without restriction, including without limitation the
|
|
||||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
||||||
sell copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice (including the next
|
|
||||||
paragraph) shall be included in all copies or substantial portions of the
|
|
||||||
Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
||||||
IN THE SOFTWARE.
|
|
||||||
|
|
||||||
The following license is from a Linux kernel header file and there is no GPL
|
|
||||||
code this package links to.
|
|
||||||
|
|
||||||
Copyright (c) 1999-2002 Vojtech Pavlik
|
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify it
|
|
||||||
under the terms of the GNU General Public License version 2 as published by
|
|
||||||
the Free Software Foundation.
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
# libevdev (vendored)
|
|
||||||
|
|
||||||
The Linux gamepad backend (`implementations/Crafter.Graphics-Gamepad.cpp`) parses evdev events with
|
|
||||||
[libevdev](https://gitlab.freedesktop.org/libevdev/libevdev). It is compiled into Crafter.Graphics from
|
|
||||||
this directory instead of being linked from the system. The Flatpak SDK that crafter-build runs in has no
|
|
||||||
libevdev headers, and a vendored copy builds the same way for every target, including cross sysroots.
|
|
||||||
|
|
||||||
- Upstream: `https://gitlab.freedesktop.org/libevdev/libevdev.git`, tag `libevdev-1.14.0`
|
|
||||||
(commit `d9af2e4580297e9c14715fef06621dd39d9d8a3a`). MIT, see `COPYING`.
|
|
||||||
- `libevdev/`: the core library sources, unchanged. The uinput half isn't used and isn't included.
|
|
||||||
- `libevdev/event-names.h`: generated, the way libevdev's meson build does it, from the kernel headers
|
|
||||||
below.
|
|
||||||
- `libevdev/config.h`: what libevdev's meson build writes on Linux (`_GNU_SOURCE`).
|
|
||||||
- `kernel-headers/linux/`: the kernel uapi `input.h` and `input-event-codes.h` that ship with that
|
|
||||||
libevdev release. libevdev compiles against its own copy (its meson build puts them first on the path),
|
|
||||||
because `event-names.h` names every code by its macro and has to see the same set of codes. Only the
|
|
||||||
libevdev library sees them, and the directory isn't called `include`, because Crafter.Build puts every
|
|
||||||
`include` directory of a dependency on its consumers' include path.
|
|
||||||
|
|
||||||
`project.cpp` builds these sources once, as their own static library, which Crafter.Graphics depends on.
|
|
||||||
|
|
||||||
## Updating
|
|
||||||
|
|
||||||
Copy `COPYING`, the files under `libevdev/` and upstream's `include/linux/linux/{input.h,input-event-codes.h}`
|
|
||||||
(into `kernel-headers/linux/`) from the new release, keep `config.h`, then regenerate the name table:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
python3 libevdev/make-event-names.py kernel-headers/linux/input.h kernel-headers/linux/input-event-codes.h \
|
|
||||||
> libevdev/event-names.h
|
|
||||||
```
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,539 +0,0 @@
|
||||||
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
|
|
||||||
/*
|
|
||||||
* Copyright (c) 1999-2002 Vojtech Pavlik
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or modify it
|
|
||||||
* under the terms of the GNU General Public License version 2 as published by
|
|
||||||
* the Free Software Foundation.
|
|
||||||
*/
|
|
||||||
#ifndef _UAPI_INPUT_H
|
|
||||||
#define _UAPI_INPUT_H
|
|
||||||
|
|
||||||
|
|
||||||
#ifndef __KERNEL__
|
|
||||||
#include <sys/time.h>
|
|
||||||
#include <sys/ioctl.h>
|
|
||||||
#include <sys/types.h>
|
|
||||||
#include <linux/types.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include "input-event-codes.h"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The event structure itself
|
|
||||||
* Note that __USE_TIME_BITS64 is defined by libc based on
|
|
||||||
* application's request to use 64 bit time_t.
|
|
||||||
*/
|
|
||||||
|
|
||||||
struct input_event {
|
|
||||||
#if (__BITS_PER_LONG != 32 || !defined(__USE_TIME_BITS64)) && !defined(__KERNEL__)
|
|
||||||
struct timeval time;
|
|
||||||
#define input_event_sec time.tv_sec
|
|
||||||
#define input_event_usec time.tv_usec
|
|
||||||
#else
|
|
||||||
__kernel_ulong_t __sec;
|
|
||||||
#if defined(__sparc__) && defined(__arch64__)
|
|
||||||
unsigned int __usec;
|
|
||||||
unsigned int __pad;
|
|
||||||
#else
|
|
||||||
__kernel_ulong_t __usec;
|
|
||||||
#endif
|
|
||||||
#define input_event_sec __sec
|
|
||||||
#define input_event_usec __usec
|
|
||||||
#endif
|
|
||||||
__u16 type;
|
|
||||||
__u16 code;
|
|
||||||
__s32 value;
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Protocol version.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#define EV_VERSION 0x010001
|
|
||||||
|
|
||||||
/*
|
|
||||||
* IOCTLs (0x00 - 0x7f)
|
|
||||||
*/
|
|
||||||
|
|
||||||
struct input_id {
|
|
||||||
__u16 bustype;
|
|
||||||
__u16 vendor;
|
|
||||||
__u16 product;
|
|
||||||
__u16 version;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* struct input_absinfo - used by EVIOCGABS/EVIOCSABS ioctls
|
|
||||||
* @value: latest reported value for the axis.
|
|
||||||
* @minimum: specifies minimum value for the axis.
|
|
||||||
* @maximum: specifies maximum value for the axis.
|
|
||||||
* @fuzz: specifies fuzz value that is used to filter noise from
|
|
||||||
* the event stream.
|
|
||||||
* @flat: values that are within this value will be discarded by
|
|
||||||
* joydev interface and reported as 0 instead.
|
|
||||||
* @resolution: specifies resolution for the values reported for
|
|
||||||
* the axis.
|
|
||||||
*
|
|
||||||
* Note that input core does not clamp reported values to the
|
|
||||||
* [minimum, maximum] limits, such task is left to userspace.
|
|
||||||
*
|
|
||||||
* The default resolution for main axes (ABS_X, ABS_Y, ABS_Z,
|
|
||||||
* ABS_MT_POSITION_X, ABS_MT_POSITION_Y) is reported in units
|
|
||||||
* per millimeter (units/mm), resolution for rotational axes
|
|
||||||
* (ABS_RX, ABS_RY, ABS_RZ) is reported in units per radian.
|
|
||||||
* The resolution for the size axes (ABS_MT_TOUCH_MAJOR,
|
|
||||||
* ABS_MT_TOUCH_MINOR, ABS_MT_WIDTH_MAJOR, ABS_MT_WIDTH_MINOR)
|
|
||||||
* is reported in units per millimeter (units/mm).
|
|
||||||
* When INPUT_PROP_ACCELEROMETER is set the resolution changes.
|
|
||||||
* The main axes (ABS_X, ABS_Y, ABS_Z) are then reported in
|
|
||||||
* units per g (units/g) and in units per degree per second
|
|
||||||
* (units/deg/s) for rotational axes (ABS_RX, ABS_RY, ABS_RZ).
|
|
||||||
*/
|
|
||||||
struct input_absinfo {
|
|
||||||
__s32 value;
|
|
||||||
__s32 minimum;
|
|
||||||
__s32 maximum;
|
|
||||||
__s32 fuzz;
|
|
||||||
__s32 flat;
|
|
||||||
__s32 resolution;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* struct input_keymap_entry - used by EVIOCGKEYCODE/EVIOCSKEYCODE ioctls
|
|
||||||
* @scancode: scancode represented in machine-endian form.
|
|
||||||
* @len: length of the scancode that resides in @scancode buffer.
|
|
||||||
* @index: index in the keymap, may be used instead of scancode
|
|
||||||
* @flags: allows to specify how kernel should handle the request. For
|
|
||||||
* example, setting INPUT_KEYMAP_BY_INDEX flag indicates that kernel
|
|
||||||
* should perform lookup in keymap by @index instead of @scancode
|
|
||||||
* @keycode: key code assigned to this scancode
|
|
||||||
*
|
|
||||||
* The structure is used to retrieve and modify keymap data. Users have
|
|
||||||
* option of performing lookup either by @scancode itself or by @index
|
|
||||||
* in keymap entry. EVIOCGKEYCODE will also return scancode or index
|
|
||||||
* (depending on which element was used to perform lookup).
|
|
||||||
*/
|
|
||||||
struct input_keymap_entry {
|
|
||||||
#define INPUT_KEYMAP_BY_INDEX (1 << 0)
|
|
||||||
__u8 flags;
|
|
||||||
__u8 len;
|
|
||||||
__u16 index;
|
|
||||||
__u32 keycode;
|
|
||||||
__u8 scancode[32];
|
|
||||||
};
|
|
||||||
|
|
||||||
struct input_mask {
|
|
||||||
__u32 type;
|
|
||||||
__u32 codes_size;
|
|
||||||
__u64 codes_ptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
#define EVIOCGVERSION _IOR('E', 0x01, int) /* get driver version */
|
|
||||||
#define EVIOCGID _IOR('E', 0x02, struct input_id) /* get device ID */
|
|
||||||
#define EVIOCGREP _IOR('E', 0x03, unsigned int[2]) /* get repeat settings */
|
|
||||||
#define EVIOCSREP _IOW('E', 0x03, unsigned int[2]) /* set repeat settings */
|
|
||||||
|
|
||||||
#define EVIOCGKEYCODE _IOR('E', 0x04, unsigned int[2]) /* get keycode */
|
|
||||||
#define EVIOCGKEYCODE_V2 _IOR('E', 0x04, struct input_keymap_entry)
|
|
||||||
#define EVIOCSKEYCODE _IOW('E', 0x04, unsigned int[2]) /* set keycode */
|
|
||||||
#define EVIOCSKEYCODE_V2 _IOW('E', 0x04, struct input_keymap_entry)
|
|
||||||
|
|
||||||
#define EVIOCGNAME(len) _IOC(_IOC_READ, 'E', 0x06, len) /* get device name */
|
|
||||||
#define EVIOCGPHYS(len) _IOC(_IOC_READ, 'E', 0x07, len) /* get physical location */
|
|
||||||
#define EVIOCGUNIQ(len) _IOC(_IOC_READ, 'E', 0x08, len) /* get unique identifier */
|
|
||||||
#define EVIOCGPROP(len) _IOC(_IOC_READ, 'E', 0x09, len) /* get device properties */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* EVIOCGMTSLOTS(len) - get MT slot values
|
|
||||||
* @len: size of the data buffer in bytes
|
|
||||||
*
|
|
||||||
* The ioctl buffer argument should be binary equivalent to
|
|
||||||
*
|
|
||||||
* struct input_mt_request_layout {
|
|
||||||
* __u32 code;
|
|
||||||
* __s32 values[num_slots];
|
|
||||||
* };
|
|
||||||
*
|
|
||||||
* where num_slots is the (arbitrary) number of MT slots to extract.
|
|
||||||
*
|
|
||||||
* The ioctl size argument (len) is the size of the buffer, which
|
|
||||||
* should satisfy len = (num_slots + 1) * sizeof(__s32). If len is
|
|
||||||
* too small to fit all available slots, the first num_slots are
|
|
||||||
* returned.
|
|
||||||
*
|
|
||||||
* Before the call, code is set to the wanted ABS_MT event type. On
|
|
||||||
* return, values[] is filled with the slot values for the specified
|
|
||||||
* ABS_MT code.
|
|
||||||
*
|
|
||||||
* If the request code is not an ABS_MT value, -EINVAL is returned.
|
|
||||||
*/
|
|
||||||
#define EVIOCGMTSLOTS(len) _IOC(_IOC_READ, 'E', 0x0a, len)
|
|
||||||
|
|
||||||
#define EVIOCGKEY(len) _IOC(_IOC_READ, 'E', 0x18, len) /* get global key state */
|
|
||||||
#define EVIOCGLED(len) _IOC(_IOC_READ, 'E', 0x19, len) /* get all LEDs */
|
|
||||||
#define EVIOCGSND(len) _IOC(_IOC_READ, 'E', 0x1a, len) /* get all sounds status */
|
|
||||||
#define EVIOCGSW(len) _IOC(_IOC_READ, 'E', 0x1b, len) /* get all switch states */
|
|
||||||
|
|
||||||
#define EVIOCGBIT(ev,len) _IOC(_IOC_READ, 'E', 0x20 + (ev), len) /* get event bits */
|
|
||||||
#define EVIOCGABS(abs) _IOR('E', 0x40 + (abs), struct input_absinfo) /* get abs value/limits */
|
|
||||||
#define EVIOCSABS(abs) _IOW('E', 0xc0 + (abs), struct input_absinfo) /* set abs value/limits */
|
|
||||||
|
|
||||||
#define EVIOCSFF _IOW('E', 0x80, struct ff_effect) /* send a force effect to a force feedback device */
|
|
||||||
#define EVIOCRMFF _IOW('E', 0x81, int) /* Erase a force effect */
|
|
||||||
#define EVIOCGEFFECTS _IOR('E', 0x84, int) /* Report number of effects playable at the same time */
|
|
||||||
|
|
||||||
#define EVIOCGRAB _IOW('E', 0x90, int) /* Grab/Release device */
|
|
||||||
#define EVIOCREVOKE _IOW('E', 0x91, int) /* Revoke device access */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* EVIOCGMASK - Retrieve current event mask
|
|
||||||
*
|
|
||||||
* This ioctl allows user to retrieve the current event mask for specific
|
|
||||||
* event type. The argument must be of type "struct input_mask" and
|
|
||||||
* specifies the event type to query, the address of the receive buffer and
|
|
||||||
* the size of the receive buffer.
|
|
||||||
*
|
|
||||||
* The event mask is a per-client mask that specifies which events are
|
|
||||||
* forwarded to the client. Each event code is represented by a single bit
|
|
||||||
* in the event mask. If the bit is set, the event is passed to the client
|
|
||||||
* normally. Otherwise, the event is filtered and will never be queued on
|
|
||||||
* the client's receive buffer.
|
|
||||||
*
|
|
||||||
* Event masks do not affect global state of the input device. They only
|
|
||||||
* affect the file descriptor they are applied to.
|
|
||||||
*
|
|
||||||
* The default event mask for a client has all bits set, i.e. all events
|
|
||||||
* are forwarded to the client. If the kernel is queried for an unknown
|
|
||||||
* event type or if the receive buffer is larger than the number of
|
|
||||||
* event codes known to the kernel, the kernel returns all zeroes for those
|
|
||||||
* codes.
|
|
||||||
*
|
|
||||||
* At maximum, codes_size bytes are copied.
|
|
||||||
*
|
|
||||||
* This ioctl may fail with ENODEV in case the file is revoked, EFAULT
|
|
||||||
* if the receive-buffer points to invalid memory, or EINVAL if the kernel
|
|
||||||
* does not implement the ioctl.
|
|
||||||
*/
|
|
||||||
#define EVIOCGMASK _IOR('E', 0x92, struct input_mask) /* Get event-masks */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* EVIOCSMASK - Set event mask
|
|
||||||
*
|
|
||||||
* This ioctl is the counterpart to EVIOCGMASK. Instead of receiving the
|
|
||||||
* current event mask, this changes the client's event mask for a specific
|
|
||||||
* type. See EVIOCGMASK for a description of event-masks and the
|
|
||||||
* argument-type.
|
|
||||||
*
|
|
||||||
* This ioctl provides full forward compatibility. If the passed event type
|
|
||||||
* is unknown to the kernel, or if the number of event codes specified in
|
|
||||||
* the mask is bigger than what is known to the kernel, the ioctl is still
|
|
||||||
* accepted and applied. However, any unknown codes are left untouched and
|
|
||||||
* stay cleared. That means, the kernel always filters unknown codes
|
|
||||||
* regardless of what the client requests. If the new mask doesn't cover
|
|
||||||
* all known event-codes, all remaining codes are automatically cleared and
|
|
||||||
* thus filtered.
|
|
||||||
*
|
|
||||||
* This ioctl may fail with ENODEV in case the file is revoked. EFAULT is
|
|
||||||
* returned if the receive-buffer points to invalid memory. EINVAL is returned
|
|
||||||
* if the kernel does not implement the ioctl.
|
|
||||||
*/
|
|
||||||
#define EVIOCSMASK _IOW('E', 0x93, struct input_mask) /* Set event-masks */
|
|
||||||
|
|
||||||
#define EVIOCSCLOCKID _IOW('E', 0xa0, int) /* Set clockid to be used for timestamps */
|
|
||||||
|
|
||||||
/*
|
|
||||||
* IDs.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#define ID_BUS 0
|
|
||||||
#define ID_VENDOR 1
|
|
||||||
#define ID_PRODUCT 2
|
|
||||||
#define ID_VERSION 3
|
|
||||||
|
|
||||||
#define BUS_PCI 0x01
|
|
||||||
#define BUS_ISAPNP 0x02
|
|
||||||
#define BUS_USB 0x03
|
|
||||||
#define BUS_HIL 0x04
|
|
||||||
#define BUS_BLUETOOTH 0x05
|
|
||||||
#define BUS_VIRTUAL 0x06
|
|
||||||
|
|
||||||
#define BUS_ISA 0x10
|
|
||||||
#define BUS_I8042 0x11
|
|
||||||
#define BUS_XTKBD 0x12
|
|
||||||
#define BUS_RS232 0x13
|
|
||||||
#define BUS_GAMEPORT 0x14
|
|
||||||
#define BUS_PARPORT 0x15
|
|
||||||
#define BUS_AMIGA 0x16
|
|
||||||
#define BUS_ADB 0x17
|
|
||||||
#define BUS_I2C 0x18
|
|
||||||
#define BUS_HOST 0x19
|
|
||||||
#define BUS_GSC 0x1A
|
|
||||||
#define BUS_ATARI 0x1B
|
|
||||||
#define BUS_SPI 0x1C
|
|
||||||
#define BUS_RMI 0x1D
|
|
||||||
#define BUS_CEC 0x1E
|
|
||||||
#define BUS_INTEL_ISHTP 0x1F
|
|
||||||
#define BUS_AMD_SFH 0x20
|
|
||||||
#define BUS_SDW 0x21
|
|
||||||
|
|
||||||
/*
|
|
||||||
* MT_TOOL types
|
|
||||||
*/
|
|
||||||
#define MT_TOOL_FINGER 0x00
|
|
||||||
#define MT_TOOL_PEN 0x01
|
|
||||||
#define MT_TOOL_PALM 0x02
|
|
||||||
#define MT_TOOL_DIAL 0x0a
|
|
||||||
#define MT_TOOL_MAX 0x0f
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Values describing the status of a force-feedback effect
|
|
||||||
*/
|
|
||||||
#define FF_STATUS_STOPPED 0x00
|
|
||||||
#define FF_STATUS_PLAYING 0x01
|
|
||||||
#define FF_STATUS_MAX 0x01
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Structures used in ioctls to upload effects to a device
|
|
||||||
* They are pieces of a bigger structure (called ff_effect)
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* All duration values are expressed in ms. Values above 32767 ms (0x7fff)
|
|
||||||
* should not be used and have unspecified results.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* struct ff_replay - defines scheduling of the force-feedback effect
|
|
||||||
* @length: duration of the effect
|
|
||||||
* @delay: delay before effect should start playing
|
|
||||||
*/
|
|
||||||
struct ff_replay {
|
|
||||||
__u16 length;
|
|
||||||
__u16 delay;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* struct ff_trigger - defines what triggers the force-feedback effect
|
|
||||||
* @button: number of the button triggering the effect
|
|
||||||
* @interval: controls how soon the effect can be re-triggered
|
|
||||||
*/
|
|
||||||
struct ff_trigger {
|
|
||||||
__u16 button;
|
|
||||||
__u16 interval;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* struct ff_envelope - generic force-feedback effect envelope
|
|
||||||
* @attack_length: duration of the attack (ms)
|
|
||||||
* @attack_level: level at the beginning of the attack
|
|
||||||
* @fade_length: duration of fade (ms)
|
|
||||||
* @fade_level: level at the end of fade
|
|
||||||
*
|
|
||||||
* The @attack_level and @fade_level are absolute values; when applying
|
|
||||||
* envelope force-feedback core will convert to positive/negative
|
|
||||||
* value based on polarity of the default level of the effect.
|
|
||||||
* Valid range for the attack and fade levels is 0x0000 - 0x7fff
|
|
||||||
*/
|
|
||||||
struct ff_envelope {
|
|
||||||
__u16 attack_length;
|
|
||||||
__u16 attack_level;
|
|
||||||
__u16 fade_length;
|
|
||||||
__u16 fade_level;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* struct ff_constant_effect - defines parameters of a constant force-feedback effect
|
|
||||||
* @level: strength of the effect; may be negative
|
|
||||||
* @envelope: envelope data
|
|
||||||
*/
|
|
||||||
struct ff_constant_effect {
|
|
||||||
__s16 level;
|
|
||||||
struct ff_envelope envelope;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* struct ff_ramp_effect - defines parameters of a ramp force-feedback effect
|
|
||||||
* @start_level: beginning strength of the effect; may be negative
|
|
||||||
* @end_level: final strength of the effect; may be negative
|
|
||||||
* @envelope: envelope data
|
|
||||||
*/
|
|
||||||
struct ff_ramp_effect {
|
|
||||||
__s16 start_level;
|
|
||||||
__s16 end_level;
|
|
||||||
struct ff_envelope envelope;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* struct ff_condition_effect - defines a spring or friction force-feedback effect
|
|
||||||
* @right_saturation: maximum level when joystick moved all way to the right
|
|
||||||
* @left_saturation: same for the left side
|
|
||||||
* @right_coeff: controls how fast the force grows when the joystick moves
|
|
||||||
* to the right
|
|
||||||
* @left_coeff: same for the left side
|
|
||||||
* @deadband: size of the dead zone, where no force is produced
|
|
||||||
* @center: position of the dead zone
|
|
||||||
*/
|
|
||||||
struct ff_condition_effect {
|
|
||||||
__u16 right_saturation;
|
|
||||||
__u16 left_saturation;
|
|
||||||
|
|
||||||
__s16 right_coeff;
|
|
||||||
__s16 left_coeff;
|
|
||||||
|
|
||||||
__u16 deadband;
|
|
||||||
__s16 center;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* struct ff_periodic_effect - defines parameters of a periodic force-feedback effect
|
|
||||||
* @waveform: kind of the effect (wave)
|
|
||||||
* @period: period of the wave (ms)
|
|
||||||
* @magnitude: peak value
|
|
||||||
* @offset: mean value of the wave (roughly)
|
|
||||||
* @phase: 'horizontal' shift
|
|
||||||
* @envelope: envelope data
|
|
||||||
* @custom_len: number of samples (FF_CUSTOM only)
|
|
||||||
* @custom_data: buffer of samples (FF_CUSTOM only)
|
|
||||||
*
|
|
||||||
* Known waveforms - FF_SQUARE, FF_TRIANGLE, FF_SINE, FF_SAW_UP,
|
|
||||||
* FF_SAW_DOWN, FF_CUSTOM. The exact syntax FF_CUSTOM is undefined
|
|
||||||
* for the time being as no driver supports it yet.
|
|
||||||
*
|
|
||||||
* Note: the data pointed by custom_data is copied by the driver.
|
|
||||||
* You can therefore dispose of the memory after the upload/update.
|
|
||||||
*/
|
|
||||||
struct ff_periodic_effect {
|
|
||||||
__u16 waveform;
|
|
||||||
__u16 period;
|
|
||||||
__s16 magnitude;
|
|
||||||
__s16 offset;
|
|
||||||
__u16 phase;
|
|
||||||
|
|
||||||
struct ff_envelope envelope;
|
|
||||||
|
|
||||||
__u32 custom_len;
|
|
||||||
__s16 *custom_data;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* struct ff_rumble_effect - defines parameters of a periodic force-feedback effect
|
|
||||||
* @strong_magnitude: magnitude of the heavy motor
|
|
||||||
* @weak_magnitude: magnitude of the light one
|
|
||||||
*
|
|
||||||
* Some rumble pads have two motors of different weight. Strong_magnitude
|
|
||||||
* represents the magnitude of the vibration generated by the heavy one.
|
|
||||||
*/
|
|
||||||
struct ff_rumble_effect {
|
|
||||||
__u16 strong_magnitude;
|
|
||||||
__u16 weak_magnitude;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* struct ff_haptic_effect
|
|
||||||
* @hid_usage: hid_usage according to Haptics page (WAVEFORM_CLICK, etc.)
|
|
||||||
* @vendor_id: the waveform vendor ID if hid_usage is in the vendor-defined range
|
|
||||||
* @vendor_waveform_page: the vendor waveform page if hid_usage is in the vendor-defined range
|
|
||||||
* @intensity: strength of the effect as percentage
|
|
||||||
* @repeat_count: number of times to retrigger effect
|
|
||||||
* @retrigger_period: time before effect is retriggered (in ms)
|
|
||||||
*/
|
|
||||||
struct ff_haptic_effect {
|
|
||||||
__u16 hid_usage;
|
|
||||||
__u16 vendor_id;
|
|
||||||
__u8 vendor_waveform_page;
|
|
||||||
__u16 intensity;
|
|
||||||
__u16 repeat_count;
|
|
||||||
__u16 retrigger_period;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* struct ff_effect - defines force feedback effect
|
|
||||||
* @type: type of the effect (FF_CONSTANT, FF_PERIODIC, FF_RAMP, FF_SPRING,
|
|
||||||
* FF_FRICTION, FF_DAMPER, FF_RUMBLE, FF_INERTIA, or FF_CUSTOM)
|
|
||||||
* @id: an unique id assigned to an effect
|
|
||||||
* @direction: direction of the effect
|
|
||||||
* @trigger: trigger conditions (struct ff_trigger)
|
|
||||||
* @replay: scheduling of the effect (struct ff_replay)
|
|
||||||
* @u: effect-specific structure (one of ff_constant_effect, ff_ramp_effect,
|
|
||||||
* ff_periodic_effect, ff_condition_effect, ff_rumble_effect) further
|
|
||||||
* defining effect parameters
|
|
||||||
*
|
|
||||||
* This structure is sent through ioctl from the application to the driver.
|
|
||||||
* To create a new effect application should set its @id to -1; the kernel
|
|
||||||
* will return assigned @id which can later be used to update or delete
|
|
||||||
* this effect.
|
|
||||||
*
|
|
||||||
* Direction of the effect is encoded as follows:
|
|
||||||
* 0 deg -> 0x0000 (down)
|
|
||||||
* 90 deg -> 0x4000 (left)
|
|
||||||
* 180 deg -> 0x8000 (up)
|
|
||||||
* 270 deg -> 0xC000 (right)
|
|
||||||
*/
|
|
||||||
struct ff_effect {
|
|
||||||
__u16 type;
|
|
||||||
__s16 id;
|
|
||||||
__u16 direction;
|
|
||||||
struct ff_trigger trigger;
|
|
||||||
struct ff_replay replay;
|
|
||||||
|
|
||||||
union {
|
|
||||||
struct ff_constant_effect constant;
|
|
||||||
struct ff_ramp_effect ramp;
|
|
||||||
struct ff_periodic_effect periodic;
|
|
||||||
struct ff_condition_effect condition[2]; /* One for each axis */
|
|
||||||
struct ff_rumble_effect rumble;
|
|
||||||
struct ff_haptic_effect haptic;
|
|
||||||
} u;
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Force feedback effect types
|
|
||||||
*/
|
|
||||||
|
|
||||||
#define FF_HAPTIC 0x4f
|
|
||||||
#define FF_RUMBLE 0x50
|
|
||||||
#define FF_PERIODIC 0x51
|
|
||||||
#define FF_CONSTANT 0x52
|
|
||||||
#define FF_SPRING 0x53
|
|
||||||
#define FF_FRICTION 0x54
|
|
||||||
#define FF_DAMPER 0x55
|
|
||||||
#define FF_INERTIA 0x56
|
|
||||||
#define FF_RAMP 0x57
|
|
||||||
|
|
||||||
#define FF_EFFECT_MIN FF_HAPTIC
|
|
||||||
#define FF_EFFECT_MAX FF_RAMP
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Force feedback periodic effect types
|
|
||||||
*/
|
|
||||||
|
|
||||||
#define FF_SQUARE 0x58
|
|
||||||
#define FF_TRIANGLE 0x59
|
|
||||||
#define FF_SINE 0x5a
|
|
||||||
#define FF_SAW_UP 0x5b
|
|
||||||
#define FF_SAW_DOWN 0x5c
|
|
||||||
#define FF_CUSTOM 0x5d
|
|
||||||
|
|
||||||
#define FF_WAVEFORM_MIN FF_SQUARE
|
|
||||||
#define FF_WAVEFORM_MAX FF_CUSTOM
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Set ff device properties
|
|
||||||
*/
|
|
||||||
|
|
||||||
#define FF_GAIN 0x60
|
|
||||||
#define FF_AUTOCENTER 0x61
|
|
||||||
|
|
||||||
/*
|
|
||||||
* ff->playback(effect_id = FF_GAIN) is the first effect_id to
|
|
||||||
* cause a collision with another ff method, in this case ff->set_gain().
|
|
||||||
* Therefore the greatest safe value for effect_id is FF_GAIN - 1,
|
|
||||||
* and thus the total number of effects should never exceed FF_GAIN.
|
|
||||||
*/
|
|
||||||
#define FF_MAX_EFFECTS FF_GAIN
|
|
||||||
|
|
||||||
#define FF_MAX 0x7f
|
|
||||||
#define FF_CNT (FF_MAX+1)
|
|
||||||
|
|
||||||
#endif /* _UAPI_INPUT_H */
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
/* SPDX-License-Identifier: MIT */
|
|
||||||
/* What libevdev's meson.build writes into config.h on Linux. */
|
|
||||||
#define _GNU_SOURCE 1
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,333 +0,0 @@
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
/*
|
|
||||||
* Copyright © 2013 Red Hat, Inc.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef LIBEVDEV_INT_H
|
|
||||||
#define LIBEVDEV_INT_H
|
|
||||||
|
|
||||||
#include "config.h"
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <errno.h>
|
|
||||||
#include "libevdev.h"
|
|
||||||
#include "libevdev-util.h"
|
|
||||||
|
|
||||||
#define MAX_NAME 256
|
|
||||||
#define ABS_MT_MIN ABS_MT_SLOT
|
|
||||||
#define ABS_MT_MAX ABS_MT_TOOL_Y
|
|
||||||
#define ABS_MT_CNT (ABS_MT_MAX - ABS_MT_MIN + 1)
|
|
||||||
#define LIBEVDEV_EXPORT __attribute__((visibility("default")))
|
|
||||||
#define ALIAS(_to) __attribute__((alias(#_to)))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sync state machine:
|
|
||||||
* default state: SYNC_NONE
|
|
||||||
*
|
|
||||||
* SYNC_NONE → SYN_DROPPED or forced sync → SYNC_NEEDED
|
|
||||||
* SYNC_NEEDED → libevdev_next_event(LIBEVDEV_READ_FLAG_SYNC) → SYNC_IN_PROGRESS
|
|
||||||
* SYNC_NEEDED → libevdev_next_event(LIBEVDEV_READ_FLAG_SYNC_NONE) → SYNC_NONE
|
|
||||||
* SYNC_IN_PROGRESS → libevdev_next_event(LIBEVDEV_READ_FLAG_SYNC_NONE) → SYNC_NONE
|
|
||||||
* SYNC_IN_PROGRESS → no sync events left → SYNC_NONE
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
enum SyncState {
|
|
||||||
SYNC_NONE,
|
|
||||||
SYNC_NEEDED,
|
|
||||||
SYNC_IN_PROGRESS,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal only: log data used to send messages to the respective log
|
|
||||||
* handler. We re-use the same struct for a global and inside
|
|
||||||
* struct libevdev.
|
|
||||||
* For the global, device_handler is NULL, for per-device instance
|
|
||||||
* global_handler is NULL.
|
|
||||||
*/
|
|
||||||
struct logdata {
|
|
||||||
enum libevdev_log_priority priority; /** minimum logging priority */
|
|
||||||
libevdev_log_func_t global_handler; /** global handler function */
|
|
||||||
libevdev_device_log_func_t device_handler; /** per-device handler function */
|
|
||||||
void *userdata; /** user-defined data pointer */
|
|
||||||
};
|
|
||||||
|
|
||||||
struct libevdev {
|
|
||||||
int fd;
|
|
||||||
bool initialized;
|
|
||||||
char *name;
|
|
||||||
char *phys;
|
|
||||||
char *uniq;
|
|
||||||
struct input_id ids;
|
|
||||||
int driver_version;
|
|
||||||
unsigned long bits[NLONGS(EV_CNT)];
|
|
||||||
unsigned long props[NLONGS(INPUT_PROP_CNT)];
|
|
||||||
unsigned long key_bits[NLONGS(KEY_CNT)];
|
|
||||||
unsigned long rel_bits[NLONGS(REL_CNT)];
|
|
||||||
unsigned long abs_bits[NLONGS(ABS_CNT)];
|
|
||||||
unsigned long led_bits[NLONGS(LED_CNT)];
|
|
||||||
unsigned long msc_bits[NLONGS(MSC_CNT)];
|
|
||||||
unsigned long sw_bits[NLONGS(SW_CNT)];
|
|
||||||
unsigned long rep_bits[NLONGS(REP_CNT)]; /* convenience, always 1 */
|
|
||||||
unsigned long ff_bits[NLONGS(FF_CNT)];
|
|
||||||
unsigned long snd_bits[NLONGS(SND_CNT)];
|
|
||||||
unsigned long key_values[NLONGS(KEY_CNT)];
|
|
||||||
unsigned long led_values[NLONGS(LED_CNT)];
|
|
||||||
unsigned long sw_values[NLONGS(SW_CNT)];
|
|
||||||
struct input_absinfo abs_info[ABS_CNT];
|
|
||||||
int *mt_slot_vals; /* [num_slots * ABS_MT_CNT] */
|
|
||||||
int num_slots; /**< valid slots in mt_slot_vals */
|
|
||||||
int current_slot;
|
|
||||||
int rep_values[REP_CNT];
|
|
||||||
|
|
||||||
enum SyncState sync_state;
|
|
||||||
enum libevdev_grab_mode grabbed;
|
|
||||||
|
|
||||||
struct input_event *queue;
|
|
||||||
size_t queue_size; /**< size of queue in elements */
|
|
||||||
size_t queue_next; /**< next event index */
|
|
||||||
size_t queue_nsync; /**< number of sync events */
|
|
||||||
|
|
||||||
struct timeval last_event_time;
|
|
||||||
|
|
||||||
struct logdata log;
|
|
||||||
};
|
|
||||||
|
|
||||||
#define log_msg_cond(dev, priority, ...) \
|
|
||||||
do { \
|
|
||||||
if (_libevdev_log_priority(dev) >= priority) \
|
|
||||||
_libevdev_log_msg(dev, priority, __FILE__, __LINE__, __func__, __VA_ARGS__); \
|
|
||||||
} while(0)
|
|
||||||
|
|
||||||
#define log_error(dev, ...) log_msg_cond(dev, LIBEVDEV_LOG_ERROR, __VA_ARGS__)
|
|
||||||
#define log_info(dev, ...) log_msg_cond(dev, LIBEVDEV_LOG_INFO, __VA_ARGS__)
|
|
||||||
#define log_dbg(dev, ...) log_msg_cond(dev, LIBEVDEV_LOG_DEBUG, __VA_ARGS__)
|
|
||||||
#define log_bug(dev, ...) log_msg_cond(dev, LIBEVDEV_LOG_ERROR, "BUG: "__VA_ARGS__)
|
|
||||||
|
|
||||||
extern void
|
|
||||||
_libevdev_log_msg(const struct libevdev *dev,
|
|
||||||
enum libevdev_log_priority priority,
|
|
||||||
const char *file, int line, const char *func,
|
|
||||||
const char *format, ...) LIBEVDEV_ATTRIBUTE_PRINTF(6, 7);
|
|
||||||
extern enum libevdev_log_priority
|
|
||||||
_libevdev_log_priority(const struct libevdev *dev);
|
|
||||||
|
|
||||||
static inline void
|
|
||||||
init_event(struct libevdev *dev, struct input_event *ev, int type, int code, int value)
|
|
||||||
{
|
|
||||||
ev->input_event_sec = dev->last_event_time.tv_sec;
|
|
||||||
ev->input_event_usec = dev->last_event_time.tv_usec;
|
|
||||||
ev->type = type;
|
|
||||||
ev->code = code;
|
|
||||||
ev->value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return a pointer to the next element in the queue, or NULL if the queue
|
|
||||||
* is full.
|
|
||||||
*/
|
|
||||||
static inline struct input_event*
|
|
||||||
queue_push(struct libevdev *dev)
|
|
||||||
{
|
|
||||||
if (dev->queue_next >= dev->queue_size)
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
return &dev->queue[dev->queue_next++];
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline bool
|
|
||||||
queue_push_event(struct libevdev *dev, unsigned int type,
|
|
||||||
unsigned int code, int value)
|
|
||||||
{
|
|
||||||
struct input_event *ev = queue_push(dev);
|
|
||||||
|
|
||||||
if (ev)
|
|
||||||
init_event(dev, ev, type, code, value);
|
|
||||||
|
|
||||||
return ev != NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set ev to the last element in the queue, removing it from the queue.
|
|
||||||
*
|
|
||||||
* @return 0 on success, 1 if the queue is empty.
|
|
||||||
*/
|
|
||||||
static inline int
|
|
||||||
queue_pop(struct libevdev *dev, struct input_event *ev)
|
|
||||||
{
|
|
||||||
if (dev->queue_next == 0)
|
|
||||||
return 1;
|
|
||||||
|
|
||||||
*ev = dev->queue[--dev->queue_next];
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline int
|
|
||||||
queue_peek(struct libevdev *dev, size_t idx, struct input_event *ev)
|
|
||||||
{
|
|
||||||
if (dev->queue_next == 0 || idx > dev->queue_next)
|
|
||||||
return 1;
|
|
||||||
*ev = dev->queue[idx];
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shift the first n elements into ev and return the number of elements
|
|
||||||
* shifted.
|
|
||||||
* ev must be large enough to store n elements.
|
|
||||||
*
|
|
||||||
* @param ev The buffer to copy into, or NULL
|
|
||||||
* @return The number of elements in ev.
|
|
||||||
*/
|
|
||||||
static inline int
|
|
||||||
queue_shift_multiple(struct libevdev *dev, size_t n, struct input_event *ev)
|
|
||||||
{
|
|
||||||
size_t remaining;
|
|
||||||
|
|
||||||
if (dev->queue_next == 0)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
remaining = dev->queue_next;
|
|
||||||
n = min(n, remaining);
|
|
||||||
remaining -= n;
|
|
||||||
|
|
||||||
if (ev)
|
|
||||||
memcpy(ev, dev->queue, n * sizeof(*ev));
|
|
||||||
|
|
||||||
memmove(dev->queue, &dev->queue[n], remaining * sizeof(*dev->queue));
|
|
||||||
|
|
||||||
dev->queue_next = remaining;
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set ev to the first element in the queue, shifting everything else
|
|
||||||
* forward by one.
|
|
||||||
*
|
|
||||||
* @return 0 on success, 1 if the queue is empty.
|
|
||||||
*/
|
|
||||||
static inline int
|
|
||||||
queue_shift(struct libevdev *dev, struct input_event *ev)
|
|
||||||
{
|
|
||||||
return queue_shift_multiple(dev, 1, ev) == 1 ? 0 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline int
|
|
||||||
queue_alloc(struct libevdev *dev, size_t size)
|
|
||||||
{
|
|
||||||
if (size == 0)
|
|
||||||
return -ENOMEM;
|
|
||||||
|
|
||||||
dev->queue = calloc(size, sizeof(struct input_event));
|
|
||||||
if (!dev->queue)
|
|
||||||
return -ENOMEM;
|
|
||||||
|
|
||||||
dev->queue_size = size;
|
|
||||||
dev->queue_next = 0;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline void
|
|
||||||
queue_free(struct libevdev *dev)
|
|
||||||
{
|
|
||||||
free(dev->queue);
|
|
||||||
dev->queue_size = 0;
|
|
||||||
dev->queue_next = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline size_t
|
|
||||||
queue_num_elements(struct libevdev *dev)
|
|
||||||
{
|
|
||||||
return dev->queue_next;
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline size_t
|
|
||||||
queue_size(struct libevdev *dev)
|
|
||||||
{
|
|
||||||
return dev->queue_size;
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline size_t
|
|
||||||
queue_num_free_elements(struct libevdev *dev)
|
|
||||||
{
|
|
||||||
if (dev->queue_size == 0)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
return dev->queue_size - dev->queue_next;
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline struct input_event *
|
|
||||||
queue_next_element(struct libevdev *dev)
|
|
||||||
{
|
|
||||||
if (dev->queue_next == dev->queue_size)
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
return &dev->queue[dev->queue_next];
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline int
|
|
||||||
queue_set_num_elements(struct libevdev *dev, size_t nelem)
|
|
||||||
{
|
|
||||||
if (nelem > dev->queue_size)
|
|
||||||
return 1;
|
|
||||||
|
|
||||||
dev->queue_next = nelem;
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#define max_mask(uc, lc) \
|
|
||||||
case EV_##uc: \
|
|
||||||
*mask = dev->lc##_bits; \
|
|
||||||
max = libevdev_event_type_get_max(type); \
|
|
||||||
break;
|
|
||||||
|
|
||||||
static inline int
|
|
||||||
type_to_mask_const(const struct libevdev *dev, unsigned int type, const unsigned long **mask)
|
|
||||||
{
|
|
||||||
int max;
|
|
||||||
|
|
||||||
switch(type) {
|
|
||||||
max_mask(ABS, abs);
|
|
||||||
max_mask(REL, rel);
|
|
||||||
max_mask(KEY, key);
|
|
||||||
max_mask(LED, led);
|
|
||||||
max_mask(MSC, msc);
|
|
||||||
max_mask(SW, sw);
|
|
||||||
max_mask(FF, ff);
|
|
||||||
max_mask(REP, rep);
|
|
||||||
max_mask(SND, snd);
|
|
||||||
default:
|
|
||||||
max = -1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return max;
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline int
|
|
||||||
type_to_mask(struct libevdev *dev, unsigned int type, unsigned long **mask)
|
|
||||||
{
|
|
||||||
int max;
|
|
||||||
|
|
||||||
switch(type) {
|
|
||||||
max_mask(ABS, abs);
|
|
||||||
max_mask(REL, rel);
|
|
||||||
max_mask(KEY, key);
|
|
||||||
max_mask(LED, led);
|
|
||||||
max_mask(MSC, msc);
|
|
||||||
max_mask(SW, sw);
|
|
||||||
max_mask(FF, ff);
|
|
||||||
max_mask(REP, rep);
|
|
||||||
max_mask(SND, snd);
|
|
||||||
default:
|
|
||||||
max = -1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return max;
|
|
||||||
}
|
|
||||||
|
|
||||||
#undef max_mask
|
|
||||||
#endif
|
|
||||||
|
|
@ -1,213 +0,0 @@
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
/*
|
|
||||||
* Copyright © 2013 David Herrmann <dh.herrmann@gmail.com>
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "config.h"
|
|
||||||
#include <errno.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <strings.h>
|
|
||||||
|
|
||||||
#include "libevdev-int.h"
|
|
||||||
#include "libevdev-util.h"
|
|
||||||
#include "libevdev.h"
|
|
||||||
|
|
||||||
#include "event-names.h"
|
|
||||||
|
|
||||||
struct name_lookup {
|
|
||||||
const char *name;
|
|
||||||
size_t len;
|
|
||||||
};
|
|
||||||
|
|
||||||
static int cmp_entry(const void *vlookup, const void *ventry)
|
|
||||||
{
|
|
||||||
const struct name_lookup *lookup = vlookup;
|
|
||||||
const struct name_entry *entry = ventry;
|
|
||||||
int r;
|
|
||||||
|
|
||||||
r = strncmp(lookup->name, entry->name, lookup->len);
|
|
||||||
if (!r) {
|
|
||||||
if (entry->name[lookup->len])
|
|
||||||
r = -1;
|
|
||||||
else
|
|
||||||
r = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
static const struct name_entry*
|
|
||||||
lookup_name(const struct name_entry *array, size_t asize,
|
|
||||||
struct name_lookup *lookup)
|
|
||||||
{
|
|
||||||
const struct name_entry *entry;
|
|
||||||
|
|
||||||
entry = bsearch(lookup, array, asize, sizeof(*array), cmp_entry);
|
|
||||||
if (!entry)
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
LIBEVDEV_EXPORT int
|
|
||||||
libevdev_event_type_from_name(const char *name)
|
|
||||||
{
|
|
||||||
return libevdev_event_type_from_name_n(name, strlen(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
LIBEVDEV_EXPORT int
|
|
||||||
libevdev_event_type_from_name_n(const char *name, size_t len)
|
|
||||||
{
|
|
||||||
struct name_lookup lookup;
|
|
||||||
const struct name_entry *entry;
|
|
||||||
|
|
||||||
lookup.name = name;
|
|
||||||
lookup.len = len;
|
|
||||||
|
|
||||||
entry = lookup_name(ev_names, ARRAY_LENGTH(ev_names), &lookup);
|
|
||||||
|
|
||||||
return entry ? (int)entry->value : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int type_from_prefix(const char *name, ssize_t len)
|
|
||||||
{
|
|
||||||
const char *e;
|
|
||||||
size_t i;
|
|
||||||
ssize_t l;
|
|
||||||
|
|
||||||
/* MAX_ is not allowed, even though EV_MAX exists */
|
|
||||||
if (startswith(name, len, "MAX_", 4))
|
|
||||||
return -1;
|
|
||||||
/* BTN_ is special as there is no EV_BTN type */
|
|
||||||
if (startswith(name, len, "BTN_", 4))
|
|
||||||
return EV_KEY;
|
|
||||||
/* FF_STATUS_ is special as FF_ is a prefix of it, so test it first */
|
|
||||||
if (startswith(name, len, "FF_STATUS_", 10))
|
|
||||||
return EV_FF_STATUS;
|
|
||||||
|
|
||||||
for (i = 0; i < ARRAY_LENGTH(ev_names); ++i) {
|
|
||||||
/* skip EV_ prefix so @e is suffix of [EV_]XYZ */
|
|
||||||
e = &ev_names[i].name[3];
|
|
||||||
l = strlen(e);
|
|
||||||
|
|
||||||
/* compare prefix and test for trailing _ */
|
|
||||||
if (len > l && startswith(name, len, e, l) && name[l] == '_')
|
|
||||||
return ev_names[i].value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
LIBEVDEV_EXPORT int
|
|
||||||
libevdev_event_code_from_name(unsigned int type, const char *name)
|
|
||||||
{
|
|
||||||
return libevdev_event_code_from_name_n(type, name, strlen(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
LIBEVDEV_EXPORT int
|
|
||||||
libevdev_event_code_from_name_n(unsigned int type, const char *name, size_t len)
|
|
||||||
{
|
|
||||||
struct name_lookup lookup;
|
|
||||||
const struct name_entry *entry;
|
|
||||||
int real_type;
|
|
||||||
|
|
||||||
/* verify that @name is really of type @type */
|
|
||||||
real_type = type_from_prefix(name, len);
|
|
||||||
if (real_type < 0 || (unsigned int)real_type != type)
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
/* now look up the name @name and return the constant */
|
|
||||||
lookup.name = name;
|
|
||||||
lookup.len = len;
|
|
||||||
|
|
||||||
entry = lookup_name(code_names, ARRAY_LENGTH(code_names), &lookup);
|
|
||||||
|
|
||||||
return entry ? (int)entry->value : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
LIBEVDEV_EXPORT int
|
|
||||||
libevdev_event_value_from_name(unsigned int type, unsigned int code, const char *name)
|
|
||||||
{
|
|
||||||
return libevdev_event_value_from_name_n(type, code, name, strlen(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
LIBEVDEV_EXPORT int
|
|
||||||
libevdev_event_value_from_name_n(unsigned int type, unsigned int code, const char *name, size_t len)
|
|
||||||
{
|
|
||||||
struct name_lookup lookup;
|
|
||||||
const struct name_entry *entry;
|
|
||||||
|
|
||||||
if (type != EV_ABS || code != ABS_MT_TOOL_TYPE)
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
lookup.name = name;
|
|
||||||
lookup.len = len;
|
|
||||||
|
|
||||||
entry = lookup_name(tool_type_names, ARRAY_LENGTH(tool_type_names), &lookup);
|
|
||||||
|
|
||||||
return entry ? (int)entry->value : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
LIBEVDEV_EXPORT int
|
|
||||||
libevdev_property_from_name(const char *name)
|
|
||||||
{
|
|
||||||
return libevdev_property_from_name_n(name, strlen(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
LIBEVDEV_EXPORT int
|
|
||||||
libevdev_property_from_name_n(const char *name, size_t len)
|
|
||||||
{
|
|
||||||
struct name_lookup lookup;
|
|
||||||
const struct name_entry *entry;
|
|
||||||
|
|
||||||
lookup.name = name;
|
|
||||||
lookup.len = len;
|
|
||||||
|
|
||||||
entry = lookup_name(prop_names, ARRAY_LENGTH(prop_names), &lookup);
|
|
||||||
|
|
||||||
return entry ? (int)entry->value : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
LIBEVDEV_EXPORT int
|
|
||||||
libevdev_event_code_from_code_name(const char *name)
|
|
||||||
{
|
|
||||||
return libevdev_event_code_from_code_name_n(name, strlen(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
LIBEVDEV_EXPORT int
|
|
||||||
libevdev_event_code_from_code_name_n(const char *name, size_t len)
|
|
||||||
{
|
|
||||||
const struct name_entry *entry;
|
|
||||||
struct name_lookup lookup;
|
|
||||||
|
|
||||||
/* now look up the name @name and return the constant */
|
|
||||||
lookup.name = name;
|
|
||||||
lookup.len = len;
|
|
||||||
|
|
||||||
entry = lookup_name(code_names, ARRAY_LENGTH(code_names), &lookup);
|
|
||||||
|
|
||||||
return entry ? (int)entry->value : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
LIBEVDEV_EXPORT int
|
|
||||||
libevdev_event_type_from_code_name(const char *name)
|
|
||||||
{
|
|
||||||
return libevdev_event_type_from_code_name_n(name, strlen(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
LIBEVDEV_EXPORT int
|
|
||||||
libevdev_event_type_from_code_name_n(const char *name, size_t len)
|
|
||||||
{
|
|
||||||
const struct name_entry *entry;
|
|
||||||
struct name_lookup lookup;
|
|
||||||
|
|
||||||
/* First look up if the name exists, we dont' want to return a valid
|
|
||||||
* type for an invalid code name */
|
|
||||||
lookup.name = name;
|
|
||||||
lookup.len = len;
|
|
||||||
|
|
||||||
entry = lookup_name(code_names, ARRAY_LENGTH(code_names), &lookup);
|
|
||||||
|
|
||||||
return entry ? type_from_prefix(name, len) : -1;
|
|
||||||
}
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
/*
|
|
||||||
* Copyright © 2013 Red Hat, Inc.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef _UTIL_H_
|
|
||||||
#define _UTIL_H_
|
|
||||||
|
|
||||||
#include "config.h"
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#define LONG_BITS (sizeof(long) * 8)
|
|
||||||
#define NLONGS(x) (((x) + LONG_BITS - 1) / LONG_BITS)
|
|
||||||
#define ARRAY_LENGTH(a) (sizeof(a) / (sizeof((a)[0])))
|
|
||||||
#define unlikely(x) (__builtin_expect(!!(x),0))
|
|
||||||
|
|
||||||
#undef min
|
|
||||||
#undef max
|
|
||||||
#ifdef __GNUC__
|
|
||||||
#define min(a,b) \
|
|
||||||
({ __typeof__ (a) _a = (a); \
|
|
||||||
__typeof__ (b) _b = (b); \
|
|
||||||
_a > _b ? _b : _a; \
|
|
||||||
})
|
|
||||||
#define max(a,b) \
|
|
||||||
({ __typeof__ (a) _a = (a); \
|
|
||||||
__typeof__ (b) _b = (b); \
|
|
||||||
_a > _b ? _a : _b; \
|
|
||||||
})
|
|
||||||
#else
|
|
||||||
#define min(a,b) ((a) > (b) ? (b) : (a))
|
|
||||||
#define max(a,b) ((a) > (b) ? (a) : (b))
|
|
||||||
#endif
|
|
||||||
|
|
||||||
static inline bool
|
|
||||||
startswith(const char *str, size_t len, const char *prefix, size_t plen)
|
|
||||||
{
|
|
||||||
return len >= plen && !strncmp(str, prefix, plen);
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline int
|
|
||||||
bit_is_set(const unsigned long *array, int bit)
|
|
||||||
{
|
|
||||||
return !!(array[bit / LONG_BITS] & (1ULL << (bit % LONG_BITS)));
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline void
|
|
||||||
set_bit(unsigned long *array, int bit)
|
|
||||||
{
|
|
||||||
array[bit / LONG_BITS] |= (1ULL << (bit % LONG_BITS));
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline void
|
|
||||||
clear_bit(unsigned long *array, int bit)
|
|
||||||
{
|
|
||||||
array[bit / LONG_BITS] &= ~(1ULL << (bit % LONG_BITS));
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline void
|
|
||||||
set_bit_state(unsigned long *array, int bit, int state)
|
|
||||||
{
|
|
||||||
if (state)
|
|
||||||
set_bit(array, bit);
|
|
||||||
else
|
|
||||||
clear_bit(array, bit);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,235 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
#
|
|
||||||
# Parses linux/input.h scanning for #define KEY_FOO 134
|
|
||||||
# Prints C header files or Python files that can be used as
|
|
||||||
# mapping and lookup tables.
|
|
||||||
#
|
|
||||||
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
class Bits(object):
|
|
||||||
def __init__(self):
|
|
||||||
self.max_codes = {}
|
|
||||||
|
|
||||||
|
|
||||||
prefixes = [
|
|
||||||
"EV_",
|
|
||||||
"REL_",
|
|
||||||
"ABS_",
|
|
||||||
"KEY_",
|
|
||||||
"BTN_",
|
|
||||||
"LED_",
|
|
||||||
"SND_",
|
|
||||||
"MSC_",
|
|
||||||
"SW_",
|
|
||||||
"FF_",
|
|
||||||
"SYN_",
|
|
||||||
"REP_",
|
|
||||||
"INPUT_PROP_",
|
|
||||||
"MT_TOOL_",
|
|
||||||
]
|
|
||||||
|
|
||||||
duplicates = [
|
|
||||||
"EV_VERSION",
|
|
||||||
"BTN_MISC",
|
|
||||||
"BTN_MOUSE",
|
|
||||||
"BTN_JOYSTICK",
|
|
||||||
"BTN_GAMEPAD",
|
|
||||||
"BTN_DIGI",
|
|
||||||
"BTN_WHEEL",
|
|
||||||
"BTN_TRIGGER_HAPPY",
|
|
||||||
"SW_MAX",
|
|
||||||
"REP_MAX",
|
|
||||||
]
|
|
||||||
|
|
||||||
ignore_prefixes = [
|
|
||||||
"SND_PROFILE_",
|
|
||||||
]
|
|
||||||
|
|
||||||
btn_additional = [
|
|
||||||
[0, "BTN_A"],
|
|
||||||
[0, "BTN_B"],
|
|
||||||
[0, "BTN_X"],
|
|
||||||
[0, "BTN_Y"],
|
|
||||||
]
|
|
||||||
|
|
||||||
code_prefixes = [
|
|
||||||
"REL_",
|
|
||||||
"ABS_",
|
|
||||||
"KEY_",
|
|
||||||
"BTN_",
|
|
||||||
"LED_",
|
|
||||||
"SND_",
|
|
||||||
"MSC_",
|
|
||||||
"SW_",
|
|
||||||
"FF_",
|
|
||||||
"SYN_",
|
|
||||||
"REP_",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def print_bits(bits, prefix):
|
|
||||||
if not hasattr(bits, prefix):
|
|
||||||
return
|
|
||||||
print("static const char * const %s_map[%s_MAX + 1] = {" % (prefix, prefix.upper()))
|
|
||||||
for val, name in sorted(list(getattr(bits, prefix).items())):
|
|
||||||
print(" [%s] = \"%s\"," % (name, name))
|
|
||||||
if prefix == "key":
|
|
||||||
for val, name in sorted(list(getattr(bits, "btn").items())):
|
|
||||||
print(" [%s] = \"%s\"," % (name, name))
|
|
||||||
print("};")
|
|
||||||
print("")
|
|
||||||
|
|
||||||
|
|
||||||
def print_map(bits):
|
|
||||||
print("static const char * const * const event_type_map[EV_MAX + 1] = {")
|
|
||||||
|
|
||||||
for prefix in prefixes:
|
|
||||||
if prefix in ["BTN_", "EV_", "INPUT_PROP_", "MT_TOOL_"]:
|
|
||||||
continue
|
|
||||||
print(" [EV_%s] = %s_map," % (prefix[:-1], prefix[:-1].lower()))
|
|
||||||
|
|
||||||
print("};")
|
|
||||||
print("")
|
|
||||||
|
|
||||||
print("#if __clang__")
|
|
||||||
print("#pragma clang diagnostic push")
|
|
||||||
print("#pragma clang diagnostic ignored \"-Winitializer-overrides\"")
|
|
||||||
print("#elif __GNUC__")
|
|
||||||
print("#pragma GCC diagnostic push")
|
|
||||||
print("#pragma GCC diagnostic ignored \"-Woverride-init\"")
|
|
||||||
print("#endif")
|
|
||||||
print("static const int ev_max[EV_MAX + 1] = {")
|
|
||||||
for val in range(bits.max_codes["EV_MAX"] + 1):
|
|
||||||
if val in bits.ev:
|
|
||||||
prefix = bits.ev[val][3:]
|
|
||||||
if prefix + "_" in prefixes:
|
|
||||||
print(" %s_MAX," % prefix)
|
|
||||||
continue
|
|
||||||
print(" -1,")
|
|
||||||
print("};")
|
|
||||||
print("#if __clang__")
|
|
||||||
print("#pragma clang diagnostic pop /* \"-Winitializer-overrides\" */")
|
|
||||||
print("#elif __GNUC__")
|
|
||||||
print("#pragma GCC diagnostic pop /* \"-Woverride-init\" */")
|
|
||||||
print("#endif")
|
|
||||||
print("")
|
|
||||||
|
|
||||||
|
|
||||||
def print_lookup(bits, prefix):
|
|
||||||
if not hasattr(bits, prefix):
|
|
||||||
return
|
|
||||||
|
|
||||||
names = sorted(list(getattr(bits, prefix).items()))
|
|
||||||
if prefix == "btn":
|
|
||||||
names = names + btn_additional
|
|
||||||
|
|
||||||
# We need to manually add the _MAX codes because some are
|
|
||||||
# duplicates
|
|
||||||
maxname = "%s_MAX" % (prefix.upper())
|
|
||||||
if maxname in duplicates:
|
|
||||||
names.append((bits.max_codes[maxname], maxname))
|
|
||||||
|
|
||||||
for val, name in sorted(names, key=lambda e: e[1]):
|
|
||||||
print(" { .name = \"%s\", .value = %s }," % (name, name))
|
|
||||||
|
|
||||||
|
|
||||||
def print_lookup_table(bits):
|
|
||||||
print("struct name_entry {")
|
|
||||||
print(" const char *name;")
|
|
||||||
print(" unsigned int value;")
|
|
||||||
print("};")
|
|
||||||
print("")
|
|
||||||
print("static const struct name_entry tool_type_names[] = {")
|
|
||||||
print_lookup(bits, "mt_tool")
|
|
||||||
print("};")
|
|
||||||
print("")
|
|
||||||
print("static const struct name_entry ev_names[] = {")
|
|
||||||
print_lookup(bits, "ev")
|
|
||||||
print("};")
|
|
||||||
print("")
|
|
||||||
|
|
||||||
print("static const struct name_entry code_names[] = {")
|
|
||||||
for prefix in sorted(code_prefixes, key=lambda e: e):
|
|
||||||
print_lookup(bits, prefix[:-1].lower())
|
|
||||||
print("};")
|
|
||||||
print("")
|
|
||||||
print("static const struct name_entry prop_names[] = {")
|
|
||||||
print_lookup(bits, "input_prop")
|
|
||||||
print("};")
|
|
||||||
print("")
|
|
||||||
|
|
||||||
|
|
||||||
def print_mapping_table(bits):
|
|
||||||
print("/* THIS FILE IS GENERATED, DO NOT EDIT */")
|
|
||||||
print("")
|
|
||||||
print("#ifndef EVENT_NAMES_H")
|
|
||||||
print("#define EVENT_NAMES_H")
|
|
||||||
print("")
|
|
||||||
|
|
||||||
for prefix in prefixes:
|
|
||||||
if prefix == "BTN_":
|
|
||||||
continue
|
|
||||||
print_bits(bits, prefix[:-1].lower())
|
|
||||||
|
|
||||||
print_map(bits)
|
|
||||||
print_lookup_table(bits)
|
|
||||||
|
|
||||||
print("#endif /* EVENT_NAMES_H */")
|
|
||||||
|
|
||||||
|
|
||||||
def parse_define(bits, line):
|
|
||||||
m = re.match(r"^#define\s+(\w+)\s+(\w+)", line)
|
|
||||||
if m is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
name = m.group(1)
|
|
||||||
|
|
||||||
try:
|
|
||||||
value = int(m.group(2), 0)
|
|
||||||
except ValueError:
|
|
||||||
return
|
|
||||||
|
|
||||||
for prefix in prefixes:
|
|
||||||
if not name.startswith(prefix) or any(name.startswith(p) for p in ignore_prefixes):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if name.endswith("_MAX"):
|
|
||||||
bits.max_codes[name] = value
|
|
||||||
|
|
||||||
if name in duplicates:
|
|
||||||
return
|
|
||||||
|
|
||||||
attrname = prefix[:-1].lower()
|
|
||||||
|
|
||||||
if not hasattr(bits, attrname):
|
|
||||||
setattr(bits, attrname, {})
|
|
||||||
b = getattr(bits, attrname)
|
|
||||||
b[value] = name
|
|
||||||
|
|
||||||
|
|
||||||
def parse(lines):
|
|
||||||
bits = Bits()
|
|
||||||
for line in lines:
|
|
||||||
if not line.startswith("#define"):
|
|
||||||
continue
|
|
||||||
parse_define(bits, line)
|
|
||||||
|
|
||||||
return bits
|
|
||||||
|
|
||||||
|
|
||||||
def usage(prog):
|
|
||||||
print("Usage: %s <files>".format(prog))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
if len(sys.argv) <= 1:
|
|
||||||
usage(sys.argv[0])
|
|
||||||
sys.exit(2)
|
|
||||||
|
|
||||||
from itertools import chain
|
|
||||||
lines = chain(*[open(f).readlines() for f in sys.argv[1:]])
|
|
||||||
bits = parse(lines)
|
|
||||||
print_mapping_table(bits)
|
|
||||||
154
project.cpp
154
project.cpp
|
|
@ -80,42 +80,11 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
cfg.linkFlags.push_back("-lwayland-client");
|
cfg.linkFlags.push_back("-lwayland-client");
|
||||||
cfg.linkFlags.push_back("-lxkbcommon");
|
cfg.linkFlags.push_back("-lxkbcommon");
|
||||||
// Gamepad: libudev for hot-plug + device enumeration; libevdev
|
// Gamepad: libudev for hot-plug + device enumeration; libevdev
|
||||||
// for event parsing + axis calibration.
|
// 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("-ludev");
|
||||||
|
cfg.linkFlags.push_back("-levdev");
|
||||||
// libevdev is vendored in lib/libevdev rather than linked from the
|
cfg.compileFlags.push_back("-I/usr/include/libevdev-1.0");
|
||||||
// system: the Flatpak SDK crafter-build runs in ships no libevdev
|
|
||||||
// headers, and a vendored copy cross-builds like everything else. It
|
|
||||||
// is its own static library so it compiles once, not again in every
|
|
||||||
// test configuration (they copy cfg.cFiles). One per call, since each
|
|
||||||
// distinct argument list loads this project separately; it lives for
|
|
||||||
// the whole build like a GitProject's configuration. Its paths are
|
|
||||||
// absolute because Crafter.Build only resolves the returned
|
|
||||||
// configuration's paths against this project's root, and this
|
|
||||||
// project.cpp runs with that root as cwd.
|
|
||||||
static std::deque<Configuration> libevdevConfigs;
|
|
||||||
Configuration& libevdev = libevdevConfigs.emplace_back();
|
|
||||||
libevdev.path = fs::absolute(cfg.path);
|
|
||||||
libevdev.name = "Crafter.Graphics-libevdev";
|
|
||||||
libevdev.outputName = "Crafter.Graphics-libevdev";
|
|
||||||
libevdev.type = ConfigurationType::LibraryStatic;
|
|
||||||
libevdev.target = cfg.target;
|
|
||||||
libevdev.march = cfg.march;
|
|
||||||
libevdev.mtune = cfg.mtune;
|
|
||||||
libevdev.debug = cfg.debug;
|
|
||||||
libevdev.sysroot = cfg.sysroot;
|
|
||||||
libevdev.cFiles = {
|
|
||||||
fs::absolute("lib/libevdev/libevdev/libevdev"),
|
|
||||||
fs::absolute("lib/libevdev/libevdev/libevdev-names"),
|
|
||||||
};
|
|
||||||
// Its generated event-names.h names every input code by macro, so it
|
|
||||||
// compiles against the kernel headers that table was generated from
|
|
||||||
// rather than the target's.
|
|
||||||
libevdev.compileFlags.push_back(std::format("-I{}", fs::absolute("lib/libevdev/kernel-headers").string()));
|
|
||||||
cfg.dependencies.push_back(&libevdev);
|
|
||||||
// For Gamepad's #include <libevdev/libevdev.h>.
|
|
||||||
cfg.compileFlags.push_back(std::format("-I{}", fs::absolute("lib/libevdev").string()));
|
|
||||||
|
|
||||||
cfg.cFiles.push_back("lib/xdg-shell-protocol");
|
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/wayland-xdg-decoration-unstable-v1-client-protocol");
|
||||||
cfg.cFiles.push_back("lib/fractional-scale-v1");
|
cfg.cFiles.push_back("lib/fractional-scale-v1");
|
||||||
|
|
@ -439,69 +408,6 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
parityImpls.emplace_back("tests/UIFallbackParity/main");
|
parityImpls.emplace_back("tests/UIFallbackParity/main");
|
||||||
up.GetInterfacesAndImplementations(ifaces, parityImpls);
|
up.GetInterfacesAndImplementations(ifaces, parityImpls);
|
||||||
cfg.tests.push_back(std::move(parityTest));
|
cfg.tests.push_back(std::move(parityTest));
|
||||||
|
|
||||||
// Reading the TLAS from the descriptor heap (#7/#15): renders a
|
|
||||||
// triangle through traceRayEXT and an inline ray query and checks
|
|
||||||
// the read-back pixels, through the engine's normal path (so on
|
|
||||||
// NVIDIA through the SPIR-V rewrite). `HeapASRead --no-workaround`
|
|
||||||
// is the manual probe for whether a driver still has the fault.
|
|
||||||
// Needs the Wayland backend, a real compositor and an RT device
|
|
||||||
// (skips without RT).
|
|
||||||
Test heapAsTest;
|
|
||||||
Configuration& ha = heapAsTest.config;
|
|
||||||
ha.path = cfg.path;
|
|
||||||
ha.name = "HeapASRead";
|
|
||||||
ha.outputName = "HeapASRead";
|
|
||||||
ha.type = ConfigurationType::Executable;
|
|
||||||
ha.target = cfg.target;
|
|
||||||
ha.march = cfg.march;
|
|
||||||
ha.mtune = cfg.mtune;
|
|
||||||
ha.debug = cfg.debug;
|
|
||||||
ha.sysroot = cfg.sysroot;
|
|
||||||
ha.dependencies = cfg.dependencies;
|
|
||||||
ha.externalDependencies = cfg.externalDependencies;
|
|
||||||
ha.compileFlags = cfg.compileFlags;
|
|
||||||
ha.linkFlags = cfg.linkFlags;
|
|
||||||
ha.defines = cfg.defines;
|
|
||||||
ha.cFiles = cfg.cFiles;
|
|
||||||
ha.shaders.emplace_back(fs::path("tests/HeapASRead/heapas-raygen.glsl"), std::string("main"), ShaderType::RayGen);
|
|
||||||
ha.shaders.emplace_back(fs::path("tests/HeapASRead/heapas-closesthit.glsl"), std::string("main"), ShaderType::ClosestHit);
|
|
||||||
ha.shaders.emplace_back(fs::path("tests/HeapASRead/heapas-miss.glsl"), std::string("main"), ShaderType::Miss);
|
|
||||||
std::vector<fs::path> heapAsImpls(impls.begin(), impls.end());
|
|
||||||
heapAsImpls.emplace_back("tests/HeapASRead/main");
|
|
||||||
ha.GetInterfacesAndImplementations(ifaces, heapAsImpls);
|
|
||||||
cfg.tests.push_back(std::move(heapAsTest));
|
|
||||||
|
|
||||||
// Dynamic descriptor-heap indexing in a hit shader: a closest-hit
|
|
||||||
// picks a storage buffer and a sampled image by a runtime index
|
|
||||||
// (the same across a wave on one instance, different between
|
|
||||||
// neighbouring lanes on the other), which lost the device on
|
|
||||||
// NVIDIA 610.43.02; skips on NVIDIA drivers older than 615.78.08.
|
|
||||||
// Same requirements as HeapASRead.
|
|
||||||
Test dynIndexTest;
|
|
||||||
Configuration& di = dynIndexTest.config;
|
|
||||||
di.path = cfg.path;
|
|
||||||
di.name = "HeapDynamicIndexHit";
|
|
||||||
di.outputName = "HeapDynamicIndexHit";
|
|
||||||
di.type = ConfigurationType::Executable;
|
|
||||||
di.target = cfg.target;
|
|
||||||
di.march = cfg.march;
|
|
||||||
di.mtune = cfg.mtune;
|
|
||||||
di.debug = cfg.debug;
|
|
||||||
di.sysroot = cfg.sysroot;
|
|
||||||
di.dependencies = cfg.dependencies;
|
|
||||||
di.externalDependencies = cfg.externalDependencies;
|
|
||||||
di.compileFlags = cfg.compileFlags;
|
|
||||||
di.linkFlags = cfg.linkFlags;
|
|
||||||
di.defines = cfg.defines;
|
|
||||||
di.cFiles = cfg.cFiles;
|
|
||||||
di.shaders.emplace_back(fs::path("tests/HeapDynamicIndexHit/dynidx-raygen.glsl"), std::string("main"), ShaderType::RayGen);
|
|
||||||
di.shaders.emplace_back(fs::path("tests/HeapDynamicIndexHit/dynidx-closesthit.glsl"), std::string("main"), ShaderType::ClosestHit);
|
|
||||||
di.shaders.emplace_back(fs::path("tests/HeapDynamicIndexHit/dynidx-miss.glsl"), std::string("main"), ShaderType::Miss);
|
|
||||||
std::vector<fs::path> dynIndexImpls(impls.begin(), impls.end());
|
|
||||||
dynIndexImpls.emplace_back("tests/HeapDynamicIndexHit/main");
|
|
||||||
di.GetInterfacesAndImplementations(ifaces, dynIndexImpls);
|
|
||||||
cfg.tests.push_back(std::move(dynIndexTest));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issue #36: BLAS build options. Drives the real hardware AS-build
|
// Issue #36: BLAS build options. Drives the real hardware AS-build
|
||||||
|
|
@ -565,58 +471,6 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
tlc.GetInterfacesAndImplementations(ifaces, tlasImpls);
|
tlc.GetInterfacesAndImplementations(ifaces, tlasImpls);
|
||||||
cfg.tests.push_back(std::move(tlasTest));
|
cfg.tests.push_back(std::move(tlasTest));
|
||||||
|
|
||||||
// The barrier BuildTLAS records after the build carries the
|
|
||||||
// acceleration-structure memory dependency to the shaders that read
|
|
||||||
// the TLAS, not just execution order. BuildTlasReadBarrier is pure
|
|
||||||
// CPU logic, so no GPU device is needed at runtime.
|
|
||||||
Test tlasBarrierTest;
|
|
||||||
Configuration& tbc = tlasBarrierTest.config;
|
|
||||||
tbc.path = cfg.path;
|
|
||||||
tbc.name = "TLASBuildBarrier";
|
|
||||||
tbc.outputName = "TLASBuildBarrier";
|
|
||||||
tbc.type = ConfigurationType::Executable;
|
|
||||||
tbc.target = cfg.target;
|
|
||||||
tbc.march = cfg.march;
|
|
||||||
tbc.mtune = cfg.mtune;
|
|
||||||
tbc.debug = cfg.debug;
|
|
||||||
tbc.sysroot = cfg.sysroot;
|
|
||||||
tbc.dependencies = cfg.dependencies;
|
|
||||||
tbc.externalDependencies = cfg.externalDependencies;
|
|
||||||
tbc.compileFlags = cfg.compileFlags;
|
|
||||||
tbc.linkFlags = cfg.linkFlags;
|
|
||||||
tbc.defines = cfg.defines;
|
|
||||||
tbc.cFiles = cfg.cFiles;
|
|
||||||
std::vector<fs::path> tlasBarrierImpls(impls.begin(), impls.end());
|
|
||||||
tlasBarrierImpls.emplace_back("tests/TLASBuildBarrier/main");
|
|
||||||
tbc.GetInterfacesAndImplementations(ifaces, tlasBarrierImpls);
|
|
||||||
cfg.tests.push_back(std::move(tlasBarrierTest));
|
|
||||||
|
|
||||||
// BuildTLAS on an empty scene (no elements registered yet) builds a
|
|
||||||
// valid empty TLAS instead of creating zero-size buffers, and adding
|
|
||||||
// and removing instances afterwards still rebuilds it. Needs a Vulkan
|
|
||||||
// RT device at runtime, like TLASHighWaterMark.
|
|
||||||
Test tlasEmptyTest;
|
|
||||||
Configuration& tec = tlasEmptyTest.config;
|
|
||||||
tec.path = cfg.path;
|
|
||||||
tec.name = "TLASEmptyBuild";
|
|
||||||
tec.outputName = "TLASEmptyBuild";
|
|
||||||
tec.type = ConfigurationType::Executable;
|
|
||||||
tec.target = cfg.target;
|
|
||||||
tec.march = cfg.march;
|
|
||||||
tec.mtune = cfg.mtune;
|
|
||||||
tec.debug = cfg.debug;
|
|
||||||
tec.sysroot = cfg.sysroot;
|
|
||||||
tec.dependencies = cfg.dependencies;
|
|
||||||
tec.externalDependencies = cfg.externalDependencies;
|
|
||||||
tec.compileFlags = cfg.compileFlags;
|
|
||||||
tec.linkFlags = cfg.linkFlags;
|
|
||||||
tec.defines = cfg.defines;
|
|
||||||
tec.cFiles = cfg.cFiles;
|
|
||||||
std::vector<fs::path> tlasEmptyImpls(impls.begin(), impls.end());
|
|
||||||
tlasEmptyImpls.emplace_back("tests/TLASEmptyBuild/main");
|
|
||||||
tec.GetInterfacesAndImplementations(ifaces, tlasEmptyImpls);
|
|
||||||
cfg.tests.push_back(std::move(tlasEmptyTest));
|
|
||||||
|
|
||||||
// Issue #118: the per-frame TLAS instance+metadata host rebuild copies
|
// Issue #118: the per-frame TLAS instance+metadata host rebuild copies
|
||||||
// (and flushes) only the slots whose element's host-authored data
|
// (and flushes) only the slots whose element's host-authored data
|
||||||
// changed, tracked via RenderingElement3D::hostDataVersion against the
|
// changed, tracked via RenderingElement3D::hostDataVersion against the
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,49 @@ layout(UI_HEAP_BUFFERS, std430) readonly buffer UICircleBuf { CircleItem items[]
|
||||||
layout(UI_HEAP_BUFFERS, std430) readonly buffer UIImageBuf { ImageItem items[]; } uiImageHeap[];
|
layout(UI_HEAP_BUFFERS, std430) readonly buffer UIImageBuf { ImageItem items[]; } uiImageHeap[];
|
||||||
layout(UI_HEAP_BUFFERS, std430) readonly buffer UIGlyphBuf { GlyphItem items[]; } uiGlyphHeap[];
|
layout(UI_HEAP_BUFFERS, std430) readonly buffer UIGlyphBuf { GlyphItem items[]; } uiGlyphHeap[];
|
||||||
|
|
||||||
|
|
||||||
|
// ──── Driver workaround: per-member SSBO load ────────────────────────────
|
||||||
|
// `UIItem it = itemHeap[idx].items[i]` emits an OpLoad of a composite type
|
||||||
|
// from a descriptor-heap'd SSBO, which crashes the GPU on the NVIDIA
|
||||||
|
// VK_EXT_descriptor_heap path (verified with a 1-float struct repro).
|
||||||
|
// Reading individual members works (each becomes OpAccessChain + scalar
|
||||||
|
// OpLoad). LoadItem reassembles the struct member-by-member into a local;
|
||||||
|
// the rest of the shader then operates on a regular local var.
|
||||||
|
|
||||||
|
ImageItem LoadImageItem(uint heap, uint i) {
|
||||||
|
ImageItem it;
|
||||||
|
it.rect = uiImageHeap[heap].items[i].rect;
|
||||||
|
it.uv = uiImageHeap[heap].items[i].uv;
|
||||||
|
it.tint = uiImageHeap[heap].items[i].tint;
|
||||||
|
it.slots = uiImageHeap[heap].items[i].slots;
|
||||||
|
return it;
|
||||||
|
}
|
||||||
|
|
||||||
|
GlyphItem LoadGlpyhtem(uint heap, uint i) {
|
||||||
|
GlyphItem it;
|
||||||
|
it.rect = uiGlyphHeap[heap].items[i].rect;
|
||||||
|
it.uv = uiGlyphHeap[heap].items[i].uv;
|
||||||
|
it.color = uiGlyphHeap[heap].items[i].color;
|
||||||
|
return it;
|
||||||
|
}
|
||||||
|
|
||||||
|
CircleItem LoadCircleItem(uint heap, uint i) {
|
||||||
|
CircleItem it;
|
||||||
|
it.centerRadius = uiCircleHeap[heap].items[i].centerRadius;
|
||||||
|
it.color = uiCircleHeap[heap].items[i].color;
|
||||||
|
it.outline = uiCircleHeap[heap].items[i].outline;
|
||||||
|
return it;
|
||||||
|
}
|
||||||
|
|
||||||
|
QuadItem LoadQuadItem(uint heap, uint i) {
|
||||||
|
QuadItem it;
|
||||||
|
it.rect = uiQuadHeap[heap].items[i].rect;
|
||||||
|
it.color = uiQuadHeap[heap].items[i].color;
|
||||||
|
it.corners = uiQuadHeap[heap].items[i].corners;
|
||||||
|
it.outline = uiQuadHeap[heap].items[i].outline;
|
||||||
|
return it;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── pixel-tile dispatch model ─────────────────────────────────────────
|
// ─── pixel-tile dispatch model ─────────────────────────────────────────
|
||||||
// Standard shaders dispatch one workgroup per 8×8 screen tile. Each thread
|
// Standard shaders dispatch one workgroup per 8×8 screen tile. Each thread
|
||||||
// owns ONE pixel and iterates ALL items in order, accumulating the result
|
// owns ONE pixel and iterates ALL items in order, accumulating the result
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
//SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
#version 460
|
|
||||||
#extension GL_EXT_ray_tracing : enable
|
|
||||||
|
|
||||||
layout(location = 0) rayPayloadInEXT vec3 hitValue;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
hitValue = vec3(0.0, 1.0, 0.0);
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
//SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
#version 460
|
|
||||||
#extension GL_EXT_ray_tracing : enable
|
|
||||||
|
|
||||||
layout(location = 0) rayPayloadInEXT vec3 hitValue;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
hitValue = vec3(0.5);
|
|
||||||
}
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
//SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
#version 460
|
|
||||||
#extension GL_EXT_ray_tracing : enable
|
|
||||||
#extension GL_EXT_ray_query : enable
|
|
||||||
#extension GL_EXT_shader_image_load_formatted : enable
|
|
||||||
#extension GL_EXT_shader_explicit_arithmetic_types_int16 : enable
|
|
||||||
#extension GL_EXT_descriptor_heap : enable
|
|
||||||
#extension GL_EXT_nonuniform_qualifier : enable
|
|
||||||
|
|
||||||
// Reads the TLAS straight out of the descriptor heap through traceRayEXT and
|
|
||||||
// through an inline ray query: the two AS-read paths that lost the device on
|
|
||||||
// NVIDIA 610.43.02 (#7). `path` picks which: 0 traces the left half of the
|
|
||||||
// frame and ray queries the right half, 1 only traces, 2 only ray queries.
|
|
||||||
layout(constant_id = 0) const uint16_t bufferStart = 0us;
|
|
||||||
layout(constant_id = 1) const uint path = 0u;
|
|
||||||
layout(descriptor_heap) uniform accelerationStructureEXT topLevelAS[];
|
|
||||||
layout(descriptor_heap) uniform writeonly image2D image[];
|
|
||||||
|
|
||||||
layout(location = 0) rayPayloadEXT vec3 hitValue;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
uvec2 pixel = gl_LaunchIDEXT.xy;
|
|
||||||
uvec2 resolution = gl_LaunchSizeEXT.xy;
|
|
||||||
vec2 ndc = (vec2(pixel) + 0.5) / vec2(resolution) * 2.0 - 1.0;
|
|
||||||
|
|
||||||
// Same pinhole camera as examples/VulkanTriangle, facing +Z.
|
|
||||||
vec3 origin = vec3(0.0, 0.0, -300.0);
|
|
||||||
float aspect = float(resolution.x) / float(resolution.y);
|
|
||||||
float tanHalfFov = tan(radians(60.0) * 0.5);
|
|
||||||
vec3 direction = normalize(vec3(ndc.x * aspect * tanHalfFov, -ndc.y * tanHalfFov, 1.0));
|
|
||||||
|
|
||||||
bool trace = path == 1u || (path == 0u && pixel.x < resolution.x / 2);
|
|
||||||
vec3 colour;
|
|
||||||
if (trace) {
|
|
||||||
// Closest-hit writes green, miss writes grey.
|
|
||||||
traceRayEXT(topLevelAS[bufferStart], gl_RayFlagsNoneEXT, 0xff, 0, 0, 0,
|
|
||||||
origin, 0.001, direction, 10000.0, 0);
|
|
||||||
colour = hitValue;
|
|
||||||
} else {
|
|
||||||
rayQueryEXT query;
|
|
||||||
rayQueryInitializeEXT(query, topLevelAS[bufferStart], gl_RayFlagsOpaqueEXT, 0xff,
|
|
||||||
origin, 0.001, direction, 10000.0);
|
|
||||||
while (rayQueryProceedEXT(query)) {}
|
|
||||||
bool hit = rayQueryGetIntersectionTypeEXT(query, true) == gl_RayQueryCommittedIntersectionTriangleEXT;
|
|
||||||
// White for a hit, the miss shader's grey otherwise.
|
|
||||||
colour = hit ? vec3(1.0) : vec3(0.5);
|
|
||||||
}
|
|
||||||
imageStore(image[0], ivec2(pixel), vec4(colour, 1.0));
|
|
||||||
}
|
|
||||||
|
|
@ -1,344 +0,0 @@
|
||||||
//SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
// Regression test for reading an acceleration structure out of a
|
|
||||||
// VK_EXT_descriptor_heap heap (#7, worked around on NVIDIA in #15). It renders
|
|
||||||
// examples/VulkanTriangle's triangle, tracing the left half of the frame with
|
|
||||||
// traceRayEXT and ray querying the right half, then reads the frame back and
|
|
||||||
// checks both halves hit the triangle where it is and miss around it. As run
|
|
||||||
// by the test runner it goes through the engine's normal path, so on NVIDIA it
|
|
||||||
// covers the SPIR-V rewrite too; a CRAFTER_GRAPHICS_NO_AS_WORKAROUND inherited
|
|
||||||
// from the environment is cleared so the rewrite can't be switched off.
|
|
||||||
//
|
|
||||||
// `HeapASRead --no-workaround` instead probes whether the driver still has the
|
|
||||||
// fault: it renders each path on its own in a child process with the rewrite
|
|
||||||
// off and validation off (GPU-assisted validation instruments the shaders),
|
|
||||||
// and reports per path whether it works, renders wrong or loses the device.
|
|
||||||
// It exits 0 only when both work. On 610.43.02 both paths lost the device; on
|
|
||||||
// 615.78.08 traceRayEXT still does, and the ray query renders nothing.
|
|
||||||
//
|
|
||||||
// The validation check only means something with CRAFTER_GRAPHICS_VALIDATION
|
|
||||||
// set. Needs a live Wayland compositor and a Vulkan RT device; without RT it
|
|
||||||
// skips.
|
|
||||||
|
|
||||||
#include "vulkan/vulkan.h"
|
|
||||||
#include <cstddef>
|
|
||||||
#include <cstdio>
|
|
||||||
#include <cstdlib>
|
|
||||||
|
|
||||||
// PNG decode for the read-back frame. The library only compiles the writer
|
|
||||||
// (Window::SaveFrame); STB_IMAGE_STATIC keeps the reader private to this file.
|
|
||||||
#define STB_IMAGE_IMPLEMENTATION
|
|
||||||
#define STB_IMAGE_STATIC
|
|
||||||
#define STBI_ONLY_PNG
|
|
||||||
#include "../../lib/stb_image.h"
|
|
||||||
|
|
||||||
import Crafter.Graphics;
|
|
||||||
import Crafter.Event;
|
|
||||||
import Crafter.Math;
|
|
||||||
import std;
|
|
||||||
|
|
||||||
using namespace Crafter;
|
|
||||||
namespace fs = std::filesystem;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
int failures = 0;
|
|
||||||
|
|
||||||
void Check(bool ok, std::string_view what) {
|
|
||||||
std::println("{} {}", ok ? "PASS" : "FAIL", what);
|
|
||||||
// Flushed per line: a driver with the fault aborts the process from
|
|
||||||
// inside Render(), and the checks before it should still show.
|
|
||||||
std::fflush(stdout);
|
|
||||||
if (!ok) ++failures;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The build places the compiled shaders next to the test binary, and the
|
|
||||||
// test runner's working directory is not that directory.
|
|
||||||
fs::path ExeDir() {
|
|
||||||
return fs::read_symlink("/proc/self/exe").parent_path();
|
|
||||||
}
|
|
||||||
|
|
||||||
int RunCommand(const std::string& cmd) {
|
|
||||||
int status = std::system(cmd.c_str());
|
|
||||||
if (status == -1) return -1;
|
|
||||||
return (status & 0x7f) == 0 ? ((status >> 8) & 0xff) : 128 + (status & 0x7f);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Frame {
|
|
||||||
int width = 0;
|
|
||||||
int height = 0;
|
|
||||||
std::vector<std::uint8_t> rgba;
|
|
||||||
|
|
||||||
std::array<std::uint8_t, 3> At(int x, int y) const {
|
|
||||||
const std::size_t i = (static_cast<std::size_t>(y) * width + x) * 4;
|
|
||||||
return { rgba[i], rgba[i + 1], rgba[i + 2] };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Frame LoadFrame(const fs::path& path) {
|
|
||||||
Frame frame;
|
|
||||||
int channels = 0;
|
|
||||||
stbi_uc* pixels = stbi_load(path.string().c_str(), &frame.width, &frame.height, &channels, STBI_rgb_alpha);
|
|
||||||
if (pixels != nullptr) {
|
|
||||||
frame.rgba.assign(pixels, pixels + static_cast<std::size_t>(frame.width) * frame.height * 4);
|
|
||||||
stbi_image_free(pixels);
|
|
||||||
}
|
|
||||||
return frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Near(std::array<std::uint8_t, 3> pixel, std::array<std::uint8_t, 3> expected) {
|
|
||||||
for (std::size_t c = 0; c < 3; ++c) {
|
|
||||||
if (std::abs(static_cast<int>(pixel[c]) - static_cast<int>(expected[c])) > 8) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void CheckPixel(const Frame& frame, float fx, float fy, std::array<std::uint8_t, 3> expected, std::string_view what) {
|
|
||||||
const int x = static_cast<int>(fx * static_cast<float>(frame.width));
|
|
||||||
const int y = static_cast<int>(fy * static_cast<float>(frame.height));
|
|
||||||
const std::array<std::uint8_t, 3> pixel = frame.At(x, y);
|
|
||||||
Check(Near(pixel, expected), std::format("{} at ({}, {}) is ({}, {}, {}), expected ({}, {}, {})",
|
|
||||||
what, x, y, pixel[0], pixel[1], pixel[2], expected[0], expected[1], expected[2]));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Matches `path` in heapas-raygen.glsl.
|
|
||||||
enum class Path : std::uint32_t { Both = 0, Trace = 1, Query = 2 };
|
|
||||||
|
|
||||||
// Spec constants of the raygen, matching heapas-raygen.glsl.
|
|
||||||
struct RaygenConstants {
|
|
||||||
std::uint16_t bufferStart;
|
|
||||||
std::uint16_t pad;
|
|
||||||
std::uint32_t path;
|
|
||||||
};
|
|
||||||
|
|
||||||
constexpr std::array<std::uint8_t, 3> traceHit = {0, 255, 0};
|
|
||||||
constexpr std::array<std::uint8_t, 3> queryHit = {255, 255, 255};
|
|
||||||
constexpr std::array<std::uint8_t, 3> miss = {128, 128, 128};
|
|
||||||
|
|
||||||
// Renders the triangle through `path` into `out` and checks the pixels.
|
|
||||||
// Returns the exit code.
|
|
||||||
int Render(Path path, const fs::path& out) {
|
|
||||||
Device::Initialize();
|
|
||||||
if (!Device::rayTracingSupported) {
|
|
||||||
std::println("SKIP no Vulkan ray-tracing device");
|
|
||||||
return 77;
|
|
||||||
}
|
|
||||||
Device::validationErrorCount = 0;
|
|
||||||
if (path != Path::Both) {
|
|
||||||
Check(!Device::workaroundDescriptorHeapAS, "the AS workaround is off for the probe");
|
|
||||||
}
|
|
||||||
|
|
||||||
Window window(640, 360, "HeapASRead test");
|
|
||||||
VkCommandBuffer cmd = window.StartInit();
|
|
||||||
|
|
||||||
DescriptorHeapVulkan descriptorHeap;
|
|
||||||
descriptorHeap.Initialize(1, 1, 0);
|
|
||||||
|
|
||||||
RaygenConstants raygenConstants = {
|
|
||||||
.bufferStart = descriptorHeap.bufferStartElement,
|
|
||||||
.pad = 0,
|
|
||||||
.path = static_cast<std::uint32_t>(path),
|
|
||||||
};
|
|
||||||
std::array<VkSpecializationMapEntry, 2> raygenEntries {{
|
|
||||||
{ .constantID = 0, .offset = offsetof(RaygenConstants, bufferStart), .size = sizeof(std::uint16_t) },
|
|
||||||
{ .constantID = 1, .offset = offsetof(RaygenConstants, path), .size = sizeof(std::uint32_t) },
|
|
||||||
}};
|
|
||||||
VkSpecializationInfo raygenSpec = {
|
|
||||||
.mapEntryCount = static_cast<std::uint32_t>(raygenEntries.size()),
|
|
||||||
.pMapEntries = raygenEntries.data(),
|
|
||||||
.dataSize = sizeof(RaygenConstants),
|
|
||||||
.pData = &raygenConstants,
|
|
||||||
};
|
|
||||||
|
|
||||||
const fs::path dir = ExeDir();
|
|
||||||
std::array<VulkanShader, 3> shaders{{
|
|
||||||
{dir / "heapas-raygen.spv", "main", VK_SHADER_STAGE_RAYGEN_BIT_KHR, &raygenSpec},
|
|
||||||
{dir / "heapas-miss.spv", "main", VK_SHADER_STAGE_MISS_BIT_KHR, nullptr},
|
|
||||||
{dir / "heapas-closesthit.spv", "main", VK_SHADER_STAGE_CLOSEST_HIT_BIT_KHR, nullptr},
|
|
||||||
}};
|
|
||||||
ShaderBindingTableVulkan shaderTable;
|
|
||||||
shaderTable.Init(shaders);
|
|
||||||
|
|
||||||
std::array<VkRayTracingShaderGroupCreateInfoKHR, 1> raygenGroups {{ {
|
|
||||||
.sType = VK_STRUCTURE_TYPE_RAY_TRACING_SHADER_GROUP_CREATE_INFO_KHR,
|
|
||||||
.type = VK_RAY_TRACING_SHADER_GROUP_TYPE_GENERAL_KHR,
|
|
||||||
.generalShader = 0, .closestHitShader = VK_SHADER_UNUSED_KHR,
|
|
||||||
.anyHitShader = VK_SHADER_UNUSED_KHR, .intersectionShader = VK_SHADER_UNUSED_KHR,
|
|
||||||
} }};
|
|
||||||
std::array<VkRayTracingShaderGroupCreateInfoKHR, 1> missGroups {{ {
|
|
||||||
.sType = VK_STRUCTURE_TYPE_RAY_TRACING_SHADER_GROUP_CREATE_INFO_KHR,
|
|
||||||
.type = VK_RAY_TRACING_SHADER_GROUP_TYPE_GENERAL_KHR,
|
|
||||||
.generalShader = 1, .closestHitShader = VK_SHADER_UNUSED_KHR,
|
|
||||||
.anyHitShader = VK_SHADER_UNUSED_KHR, .intersectionShader = VK_SHADER_UNUSED_KHR,
|
|
||||||
} }};
|
|
||||||
std::array<VkRayTracingShaderGroupCreateInfoKHR, 1> hitGroups {{ {
|
|
||||||
.sType = VK_STRUCTURE_TYPE_RAY_TRACING_SHADER_GROUP_CREATE_INFO_KHR,
|
|
||||||
.type = VK_RAY_TRACING_SHADER_GROUP_TYPE_TRIANGLES_HIT_GROUP_KHR,
|
|
||||||
.generalShader = VK_SHADER_UNUSED_KHR, .closestHitShader = 2,
|
|
||||||
.anyHitShader = VK_SHADER_UNUSED_KHR, .intersectionShader = VK_SHADER_UNUSED_KHR,
|
|
||||||
} }};
|
|
||||||
PipelineRTVulkan pipeline;
|
|
||||||
pipeline.Init(cmd, raygenGroups, missGroups, hitGroups, shaderTable);
|
|
||||||
|
|
||||||
Mesh triangleMesh;
|
|
||||||
std::array<Vector<float, 3, 3>, 3> verts {{{-150, -150, 100}, {0, 150, 100}, {150, -150, 100}}};
|
|
||||||
std::array<std::uint32_t, 3> index {{2, 1, 0}};
|
|
||||||
triangleMesh.Build(verts, index, cmd);
|
|
||||||
|
|
||||||
RenderingElement3D renderer;
|
|
||||||
renderer.instance = {
|
|
||||||
.instanceCustomIndex = 0,
|
|
||||||
.mask = 0xFF,
|
|
||||||
.instanceShaderBindingTableRecordOffset = 0,
|
|
||||||
.flags = VK_GEOMETRY_INSTANCE_FORCE_OPAQUE_BIT_KHR,
|
|
||||||
.accelerationStructureReference = triangleMesh.blasAddr,
|
|
||||||
};
|
|
||||||
MatrixRowMajor<float, 4, 3, 1>::Identity().Store(reinterpret_cast<float*>(renderer.instance.transform.matrix));
|
|
||||||
RenderingElement3D::Add(&renderer);
|
|
||||||
for (std::uint32_t f = 0; f < Window::numFrames; ++f) RenderingElement3D::BuildTLAS(cmd, f);
|
|
||||||
|
|
||||||
window.FinishInit();
|
|
||||||
|
|
||||||
// Per frame: the TLAS at buffer slot 0 and that frame's swapchain image at
|
|
||||||
// image slot 0, each in its own copy of the heap. Written again whenever
|
|
||||||
// the swapchain is recreated, since the image descriptors point at its
|
|
||||||
// images.
|
|
||||||
auto imgSlots = descriptorHeap.AllocateImageSlots(1);
|
|
||||||
auto bufSlots = descriptorHeap.AllocateBufferSlots(1);
|
|
||||||
auto writeDescriptors = [&] {
|
|
||||||
std::array<VkDeviceAddressRangeKHR, Window::numFrames> tlasRanges{};
|
|
||||||
std::array<VkImageDescriptorInfoEXT, Window::numFrames> imageInfos{};
|
|
||||||
std::vector<VkResourceDescriptorInfoEXT> resources;
|
|
||||||
std::vector<VkHostAddressRangeEXT> destinations;
|
|
||||||
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
|
||||||
tlasRanges[f] = { .address = RenderingElement3D::tlases[f].address };
|
|
||||||
imageInfos[f] = {
|
|
||||||
.sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT,
|
|
||||||
.pView = &window.imageViews[f],
|
|
||||||
.layout = VK_IMAGE_LAYOUT_GENERAL,
|
|
||||||
};
|
|
||||||
resources.push_back({
|
|
||||||
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
|
||||||
.type = VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR,
|
|
||||||
.data = { .pAddressRange = &tlasRanges[f] },
|
|
||||||
});
|
|
||||||
destinations.push_back({
|
|
||||||
.address = descriptorHeap.resourceHeap[f].value + descriptorHeap.BufferByteOffset(bufSlots.firstElement),
|
|
||||||
.size = Device::descriptorHeapProperties.bufferDescriptorSize,
|
|
||||||
});
|
|
||||||
resources.push_back({
|
|
||||||
.sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
|
||||||
.type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
|
|
||||||
.data = { .pImage = &imageInfos[f] },
|
|
||||||
});
|
|
||||||
destinations.push_back({
|
|
||||||
.address = descriptorHeap.resourceHeap[f].value + descriptorHeap.ImageByteOffset(imgSlots.firstElement),
|
|
||||||
.size = Device::descriptorHeapProperties.imageDescriptorSize,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Device::vkWriteResourceDescriptorsEXT(Device::device, static_cast<std::uint32_t>(resources.size()),
|
|
||||||
resources.data(), destinations.data());
|
|
||||||
for (auto& heap : descriptorHeap.resourceHeap) heap.FlushDevice();
|
|
||||||
};
|
|
||||||
writeDescriptors();
|
|
||||||
EventListener<void> resizeListener(&window.onResize, writeDescriptors);
|
|
||||||
|
|
||||||
window.descriptorHeap = &descriptorHeap;
|
|
||||||
RTPass rtPass(&pipeline);
|
|
||||||
window.passes.push_back(&rtPass);
|
|
||||||
|
|
||||||
// More frames than are in flight, so every per-frame heap copy and TLAS
|
|
||||||
// is read at least once. A device loss here throws out of Render().
|
|
||||||
for (int i = 0; i < Window::numFrames * 2; ++i) window.Render();
|
|
||||||
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
|
||||||
|
|
||||||
// Counted before the capture: SaveFrame reads back an image that has
|
|
||||||
// already been handed to the presentation engine, which the validation
|
|
||||||
// layer reports on its own.
|
|
||||||
if (std::getenv("CRAFTER_GRAPHICS_VALIDATION") != nullptr) {
|
|
||||||
Check(Device::validationErrorCount == 0,
|
|
||||||
std::format("no Vulkan validation errors while rendering ({} seen)", Device::validationErrorCount));
|
|
||||||
}
|
|
||||||
|
|
||||||
window.SaveFrame(out);
|
|
||||||
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
|
||||||
const Frame frame = LoadFrame(out);
|
|
||||||
Check(!frame.rgba.empty(), "the frame was read back");
|
|
||||||
if (!frame.rgba.empty()) {
|
|
||||||
switch (path) {
|
|
||||||
case Path::Both:
|
|
||||||
// The triangle covers the middle of the frame at any aspect
|
|
||||||
// ratio; 5% either side of the centre line is inside it.
|
|
||||||
CheckPixel(frame, 0.45f, 0.5f, traceHit, "traceRayEXT hit");
|
|
||||||
CheckPixel(frame, 0.55f, 0.5f, queryHit, "rayQueryEXT hit");
|
|
||||||
CheckPixel(frame, 0.05f, 0.1f, miss, "traceRayEXT miss");
|
|
||||||
CheckPixel(frame, 0.95f, 0.1f, miss, "rayQueryEXT miss");
|
|
||||||
break;
|
|
||||||
case Path::Trace:
|
|
||||||
CheckPixel(frame, 0.5f, 0.5f, traceHit, "traceRayEXT hit");
|
|
||||||
CheckPixel(frame, 0.05f, 0.1f, miss, "traceRayEXT miss");
|
|
||||||
break;
|
|
||||||
case Path::Query:
|
|
||||||
CheckPixel(frame, 0.5f, 0.5f, queryHit, "rayQueryEXT hit");
|
|
||||||
CheckPixel(frame, 0.05f, 0.1f, miss, "rayQueryEXT miss");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RenderingElement3D::Remove(&renderer);
|
|
||||||
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A fresh directory for the read-back frames, so a frame left by an earlier
|
|
||||||
// run can't stand in for one this run failed to write.
|
|
||||||
fs::path OutputDir() {
|
|
||||||
const fs::path dir = fs::temp_directory_path() / "crafter-heap-as-read";
|
|
||||||
std::error_code ec;
|
|
||||||
fs::remove_all(dir, ec);
|
|
||||||
fs::create_directories(dir);
|
|
||||||
return dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --no-workaround: renders each path in its own child process, so a device
|
|
||||||
// loss on one doesn't hide the other.
|
|
||||||
int Probe() {
|
|
||||||
const fs::path exe = fs::read_symlink("/proc/self/exe");
|
|
||||||
const fs::path dir = OutputDir();
|
|
||||||
bool allWork = true;
|
|
||||||
for (const auto& [name, arg] : {std::pair{"traceRayEXT", "trace"}, std::pair{"rayQueryEXT", "query"}}) {
|
|
||||||
const int exit = RunCommand(std::format(
|
|
||||||
"env -u CRAFTER_GRAPHICS_VALIDATION CRAFTER_GRAPHICS_NO_AS_WORKAROUND=1 '{}' --probe {} '{}'",
|
|
||||||
exe.string(), arg, (dir / std::format("{}.png", arg)).string()));
|
|
||||||
const std::string verdict =
|
|
||||||
exit == 0 ? std::string("works") :
|
|
||||||
exit == 77 ? std::string("skipped (no RT device)") :
|
|
||||||
exit == 1 ? std::string("renders wrong") :
|
|
||||||
std::format("loses the device or crashes (exit {})", exit);
|
|
||||||
std::println("PROBE {} reading the TLAS from the heap: {}", name, verdict);
|
|
||||||
if (exit != 0) allWork = false;
|
|
||||||
}
|
|
||||||
std::println("{}", allWork ? "the driver reads the TLAS from the heap on both paths"
|
|
||||||
: "the driver still needs the workaround");
|
|
||||||
return allWork ? EXIT_SUCCESS : EXIT_FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
int main(int argc, char** argv) {
|
|
||||||
if (argc == 2 && std::string_view(argv[1]) == "--no-workaround") {
|
|
||||||
return Probe();
|
|
||||||
}
|
|
||||||
if (argc == 4 && std::string_view(argv[1]) == "--probe") {
|
|
||||||
const Path path = std::string_view(argv[2]) == "trace" ? Path::Trace : Path::Query;
|
|
||||||
return Render(path, argv[3]);
|
|
||||||
}
|
|
||||||
|
|
||||||
unsetenv("CRAFTER_GRAPHICS_NO_AS_WORKAROUND");
|
|
||||||
const int result = Render(Path::Both, OutputDir() / "both.png");
|
|
||||||
if (result == 77) return result;
|
|
||||||
Check(Device::workaroundDescriptorHeapAS
|
|
||||||
== (Device::descriptorHeapSupported && Device::driverProperties.driverID == VK_DRIVER_ID_NVIDIA_PROPRIETARY),
|
|
||||||
"the AS workaround is on exactly where the driver needs it");
|
|
||||||
std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed");
|
|
||||||
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
//SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
#version 460
|
|
||||||
#extension GL_EXT_ray_tracing : enable
|
|
||||||
#extension GL_EXT_shader_explicit_arithmetic_types_int16 : enable
|
|
||||||
#extension GL_EXT_descriptor_heap : enable
|
|
||||||
#extension GL_EXT_nonuniform_qualifier : enable
|
|
||||||
|
|
||||||
// Picks a storage buffer and a sampled image out of the descriptor heap by a
|
|
||||||
// runtime index. Instance 0 uses descriptor set 0 everywhere, so the index is
|
|
||||||
// the same across each wave. Instance 1 alternates between sets 1 and 0 from
|
|
||||||
// one pixel column to the next, so neighbouring lanes of every wave over it
|
|
||||||
// pick different descriptors. On NVIDIA 610.43.02 a runtime index here lost the
|
|
||||||
// device, for both descriptor kinds, which is why the engine's examples select
|
|
||||||
// hit-shader descriptors through spec constants.
|
|
||||||
layout(constant_id = 0) const uint16_t bufferStart = 0us;
|
|
||||||
layout(constant_id = 1) const uint textureBase = 0u;
|
|
||||||
layout(constant_id = 2) const uint colourBase = 0u;
|
|
||||||
|
|
||||||
layout(descriptor_heap, std430) readonly buffer Colour { vec4 value; } colours[];
|
|
||||||
layout(descriptor_heap) uniform texture2D textures[];
|
|
||||||
layout(descriptor_heap) uniform sampler samplers[];
|
|
||||||
|
|
||||||
layout(location = 0) rayPayloadInEXT vec3 hitValue;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
uint set = gl_InstanceCustomIndexEXT == 0u ? 0u : 1u - (gl_LaunchIDEXT.x & 1u);
|
|
||||||
vec4 colour = colours[nonuniformEXT(uint(bufferStart) + colourBase + set)].value;
|
|
||||||
vec4 texel = textureLod(sampler2D(textures[nonuniformEXT(textureBase + set)], samplers[0]),
|
|
||||||
vec2(0.5), 0.0);
|
|
||||||
// Red from the buffer, green from the texture, so each descriptor kind
|
|
||||||
// is checked separately.
|
|
||||||
hitValue = vec3(colour.r, texel.g, 0.0);
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
//SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
#version 460
|
|
||||||
#extension GL_EXT_ray_tracing : enable
|
|
||||||
|
|
||||||
layout(location = 0) rayPayloadInEXT vec3 hitValue;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
hitValue = vec3(0.0, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
//SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
#version 460
|
|
||||||
#extension GL_EXT_ray_tracing : enable
|
|
||||||
#extension GL_EXT_shader_image_load_formatted : enable
|
|
||||||
#extension GL_EXT_shader_explicit_arithmetic_types_int16 : enable
|
|
||||||
#extension GL_EXT_descriptor_heap : enable
|
|
||||||
#extension GL_EXT_nonuniform_qualifier : enable
|
|
||||||
|
|
||||||
layout(constant_id = 0) const uint16_t bufferStart = 0us;
|
|
||||||
layout(descriptor_heap) uniform accelerationStructureEXT topLevelAS[];
|
|
||||||
layout(descriptor_heap) uniform writeonly image2D image[];
|
|
||||||
|
|
||||||
layout(location = 0) rayPayloadEXT vec3 hitValue;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
uvec2 pixel = gl_LaunchIDEXT.xy;
|
|
||||||
uvec2 resolution = gl_LaunchSizeEXT.xy;
|
|
||||||
vec2 ndc = (vec2(pixel) + 0.5) / vec2(resolution) * 2.0 - 1.0;
|
|
||||||
|
|
||||||
// Same pinhole camera as examples/VulkanTriangle, facing +Z.
|
|
||||||
vec3 origin = vec3(0.0, 0.0, -300.0);
|
|
||||||
float aspect = float(resolution.x) / float(resolution.y);
|
|
||||||
float tanHalfFov = tan(radians(60.0) * 0.5);
|
|
||||||
vec3 direction = normalize(vec3(ndc.x * aspect * tanHalfFov, -ndc.y * tanHalfFov, 1.0));
|
|
||||||
|
|
||||||
traceRayEXT(topLevelAS[bufferStart], gl_RayFlagsNoneEXT, 0xff, 0, 0, 0,
|
|
||||||
origin, 0.001, direction, 10000.0, 0);
|
|
||||||
imageStore(image[0], ivec2(pixel), vec4(hitValue, 1.0));
|
|
||||||
}
|
|
||||||
|
|
@ -1,415 +0,0 @@
|
||||||
//SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
// Regression test for dynamic descriptor-heap indexing in ray-tracing hit
|
|
||||||
// shaders. On NVIDIA 610.43.02, selecting a VK_EXT_descriptor_heap descriptor
|
|
||||||
// with a runtime index inside a hit shader lost the device, for storage
|
|
||||||
// buffers and sampled images alike (README, "Native RT limitation"), so the
|
|
||||||
// examples select hit-shader descriptors through spec constants. This renders
|
|
||||||
// two instances of one triangle whose closest-hit picks a storage buffer and a
|
|
||||||
// texture by a runtime index: instance 0 the same set everywhere, instance 1 a
|
|
||||||
// different set in every other pixel column, so neighbouring lanes of a wave
|
|
||||||
// select different descriptors. It reads the frame back and checks every
|
|
||||||
// pixel got its own set's buffer red and texture green.
|
|
||||||
//
|
|
||||||
// The fault fixed in NVIDIA 615.78.08, so on older NVIDIA drivers the test
|
|
||||||
// skips instead of aborting the suite. GPU-assisted validation hides the
|
|
||||||
// fault (examples/Sponza/README.md), so with CRAFTER_GRAPHICS_VALIDATION set
|
|
||||||
// the scene is also rendered in a child process with validation off; the
|
|
||||||
// validation check only means something with it set. Needs a live Wayland
|
|
||||||
// compositor and a Vulkan RT device; without RT it skips.
|
|
||||||
|
|
||||||
#include "vulkan/vulkan.h"
|
|
||||||
#include <cstddef>
|
|
||||||
#include <cstdio>
|
|
||||||
#include <cstdlib>
|
|
||||||
|
|
||||||
// PNG decode for the read-back frame. The library only compiles the writer
|
|
||||||
// (Window::SaveFrame); STB_IMAGE_STATIC keeps the reader private to this file.
|
|
||||||
#define STB_IMAGE_IMPLEMENTATION
|
|
||||||
#define STB_IMAGE_STATIC
|
|
||||||
#define STBI_ONLY_PNG
|
|
||||||
#include "../../lib/stb_image.h"
|
|
||||||
|
|
||||||
import Crafter.Graphics;
|
|
||||||
import Crafter.Event;
|
|
||||||
import Crafter.Math;
|
|
||||||
import std;
|
|
||||||
|
|
||||||
using namespace Crafter;
|
|
||||||
namespace fs = std::filesystem;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
int failures = 0;
|
|
||||||
|
|
||||||
void Check(bool ok, std::string_view what) {
|
|
||||||
std::println("{} {}", ok ? "PASS" : "FAIL", what);
|
|
||||||
// Flushed per line: a driver with the fault aborts the process from
|
|
||||||
// inside Render(), and the checks before it should still show.
|
|
||||||
std::fflush(stdout);
|
|
||||||
if (!ok) ++failures;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The build places the compiled shaders next to the test binary, and the
|
|
||||||
// test runner's working directory is not that directory.
|
|
||||||
fs::path ExeDir() {
|
|
||||||
return fs::read_symlink("/proc/self/exe").parent_path();
|
|
||||||
}
|
|
||||||
|
|
||||||
int RunCommand(const std::string& cmd) {
|
|
||||||
int status = std::system(cmd.c_str());
|
|
||||||
if (status == -1) return -1;
|
|
||||||
return (status & 0x7f) == 0 ? ((status >> 8) & 0xff) : 128 + (status & 0x7f);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct RGBA8 { std::uint8_t r, g, b, a; };
|
|
||||||
|
|
||||||
struct Frame {
|
|
||||||
int width = 0;
|
|
||||||
int height = 0;
|
|
||||||
std::vector<std::uint8_t> rgba;
|
|
||||||
|
|
||||||
std::array<std::uint8_t, 3> At(int x, int y) const {
|
|
||||||
const std::size_t i = (static_cast<std::size_t>(y) * width + x) * 4;
|
|
||||||
return { rgba[i], rgba[i + 1], rgba[i + 2] };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Frame LoadFrame(const fs::path& path) {
|
|
||||||
Frame frame;
|
|
||||||
int channels = 0;
|
|
||||||
stbi_uc* pixels = stbi_load(path.string().c_str(), &frame.width, &frame.height, &channels, STBI_rgb_alpha);
|
|
||||||
if (pixels != nullptr) {
|
|
||||||
frame.rgba.assign(pixels, pixels + static_cast<std::size_t>(frame.width) * frame.height * 4);
|
|
||||||
stbi_image_free(pixels);
|
|
||||||
}
|
|
||||||
return frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Near(std::array<std::uint8_t, 3> pixel, std::array<std::uint8_t, 3> expected) {
|
|
||||||
for (std::size_t c = 0; c < 3; ++c) {
|
|
||||||
if (std::abs(static_cast<int>(pixel[c]) - static_cast<int>(expected[c])) > 8) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void CheckPixel(const Frame& frame, int x, int y, std::array<std::uint8_t, 3> expected, std::string_view what) {
|
|
||||||
const std::array<std::uint8_t, 3> pixel = frame.At(x, y);
|
|
||||||
Check(Near(pixel, expected), std::format("{} at ({}, {}) is ({}, {}, {}), expected ({}, {}, {})",
|
|
||||||
what, x, y, pixel[0], pixel[1], pixel[2], expected[0], expected[1], expected[2]));
|
|
||||||
}
|
|
||||||
|
|
||||||
// The pixel column world x on the triangles' plane (z = 100, 400 in front of
|
|
||||||
// the camera) lands in, for the raygen's 60° vertical field of view.
|
|
||||||
int FrameX(float worldX, const Frame& frame) {
|
|
||||||
const float aspect = static_cast<float>(frame.width) / static_cast<float>(frame.height);
|
|
||||||
const float ndc = worldX / (400.0f * aspect * std::tan(std::numbers::pi_v<float> / 6.0f));
|
|
||||||
return static_cast<int>((0.5f + ndc * 0.5f) * static_cast<float>(frame.width));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spec constants of the closest-hit, matching dynidx-closesthit.glsl.
|
|
||||||
struct HitConstants {
|
|
||||||
std::uint16_t bufferStart;
|
|
||||||
std::uint16_t pad;
|
|
||||||
std::uint32_t textureBase;
|
|
||||||
std::uint32_t colourBase;
|
|
||||||
};
|
|
||||||
|
|
||||||
// What each descriptor set holds: the red its buffer gives and the green its
|
|
||||||
// texture gives.
|
|
||||||
constexpr std::array<float, 2> bufferRed = {1.0f, 0.5f};
|
|
||||||
constexpr std::array<std::uint8_t, 2> textureGreen = {64, 191};
|
|
||||||
constexpr std::array<std::array<std::uint8_t, 3>, 2> setColour = {{{255, 64, 0}, {128, 191, 0}}};
|
|
||||||
|
|
||||||
// NVIDIA's proprietary driver packs its version into 10.8.8.6 bits.
|
|
||||||
std::tuple<std::uint32_t, std::uint32_t, std::uint32_t> NvidiaVersion() {
|
|
||||||
const std::uint32_t v = Device::deviceProperties.driverVersion;
|
|
||||||
return {(v >> 22) & 0x3ff, (v >> 14) & 0xff, (v >> 6) & 0xff};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Renders the scene into `out` and checks the pixels. Device::Initialize has
|
|
||||||
// already run. Returns the number of failed checks.
|
|
||||||
int RenderScene(const fs::path& out) {
|
|
||||||
const int failuresBefore = failures;
|
|
||||||
Device::validationErrorCount = 0;
|
|
||||||
|
|
||||||
Window window(640, 360, "HeapDynamicIndexHit test");
|
|
||||||
VkCommandBuffer cmd = window.StartInit();
|
|
||||||
|
|
||||||
// Image slots: 0 = the frame's swapchain image, 1-2 = the textures.
|
|
||||||
// Buffer slots: 0 = the TLAS, 1-2 = the colour buffers. Sampler slot 0.
|
|
||||||
DescriptorHeapVulkan descriptorHeap;
|
|
||||||
descriptorHeap.Initialize(3, 3, 1);
|
|
||||||
|
|
||||||
VkSpecializationMapEntry raygenEntry = { .constantID = 0, .offset = 0, .size = sizeof(std::uint16_t) };
|
|
||||||
VkSpecializationInfo raygenSpec = {
|
|
||||||
.mapEntryCount = 1,
|
|
||||||
.pMapEntries = &raygenEntry,
|
|
||||||
.dataSize = sizeof(std::uint16_t),
|
|
||||||
.pData = &descriptorHeap.bufferStartElement,
|
|
||||||
};
|
|
||||||
HitConstants hitConstants = {
|
|
||||||
.bufferStart = descriptorHeap.bufferStartElement,
|
|
||||||
.pad = 0,
|
|
||||||
.textureBase = 1,
|
|
||||||
.colourBase = 1,
|
|
||||||
};
|
|
||||||
std::array<VkSpecializationMapEntry, 3> hitEntries {{
|
|
||||||
{ .constantID = 0, .offset = offsetof(HitConstants, bufferStart), .size = sizeof(std::uint16_t) },
|
|
||||||
{ .constantID = 1, .offset = offsetof(HitConstants, textureBase), .size = sizeof(std::uint32_t) },
|
|
||||||
{ .constantID = 2, .offset = offsetof(HitConstants, colourBase), .size = sizeof(std::uint32_t) },
|
|
||||||
}};
|
|
||||||
VkSpecializationInfo hitSpec = {
|
|
||||||
.mapEntryCount = static_cast<std::uint32_t>(hitEntries.size()),
|
|
||||||
.pMapEntries = hitEntries.data(),
|
|
||||||
.dataSize = sizeof(HitConstants),
|
|
||||||
.pData = &hitConstants,
|
|
||||||
};
|
|
||||||
|
|
||||||
const fs::path dir = ExeDir();
|
|
||||||
std::array<VulkanShader, 3> shaders{{
|
|
||||||
{dir / "dynidx-raygen.spv", "main", VK_SHADER_STAGE_RAYGEN_BIT_KHR, &raygenSpec},
|
|
||||||
{dir / "dynidx-miss.spv", "main", VK_SHADER_STAGE_MISS_BIT_KHR, nullptr},
|
|
||||||
{dir / "dynidx-closesthit.spv", "main", VK_SHADER_STAGE_CLOSEST_HIT_BIT_KHR, &hitSpec},
|
|
||||||
}};
|
|
||||||
ShaderBindingTableVulkan shaderTable;
|
|
||||||
shaderTable.Init(shaders);
|
|
||||||
|
|
||||||
std::array<VkRayTracingShaderGroupCreateInfoKHR, 1> raygenGroups {{ {
|
|
||||||
.sType = VK_STRUCTURE_TYPE_RAY_TRACING_SHADER_GROUP_CREATE_INFO_KHR,
|
|
||||||
.type = VK_RAY_TRACING_SHADER_GROUP_TYPE_GENERAL_KHR,
|
|
||||||
.generalShader = 0, .closestHitShader = VK_SHADER_UNUSED_KHR,
|
|
||||||
.anyHitShader = VK_SHADER_UNUSED_KHR, .intersectionShader = VK_SHADER_UNUSED_KHR,
|
|
||||||
} }};
|
|
||||||
std::array<VkRayTracingShaderGroupCreateInfoKHR, 1> missGroups {{ {
|
|
||||||
.sType = VK_STRUCTURE_TYPE_RAY_TRACING_SHADER_GROUP_CREATE_INFO_KHR,
|
|
||||||
.type = VK_RAY_TRACING_SHADER_GROUP_TYPE_GENERAL_KHR,
|
|
||||||
.generalShader = 1, .closestHitShader = VK_SHADER_UNUSED_KHR,
|
|
||||||
.anyHitShader = VK_SHADER_UNUSED_KHR, .intersectionShader = VK_SHADER_UNUSED_KHR,
|
|
||||||
} }};
|
|
||||||
std::array<VkRayTracingShaderGroupCreateInfoKHR, 1> hitGroups {{ {
|
|
||||||
.sType = VK_STRUCTURE_TYPE_RAY_TRACING_SHADER_GROUP_CREATE_INFO_KHR,
|
|
||||||
.type = VK_RAY_TRACING_SHADER_GROUP_TYPE_TRIANGLES_HIT_GROUP_KHR,
|
|
||||||
.generalShader = VK_SHADER_UNUSED_KHR, .closestHitShader = 2,
|
|
||||||
.anyHitShader = VK_SHADER_UNUSED_KHR, .intersectionShader = VK_SHADER_UNUSED_KHR,
|
|
||||||
} }};
|
|
||||||
PipelineRTVulkan pipeline;
|
|
||||||
pipeline.Init(cmd, raygenGroups, missGroups, hitGroups, shaderTable);
|
|
||||||
|
|
||||||
// One triangle, two instances 100 units either side of the centre line.
|
|
||||||
Mesh triangleMesh;
|
|
||||||
std::array<Vector<float, 3, 3>, 3> verts {{{-75, -75, 100}, {0, 75, 100}, {75, -75, 100}}};
|
|
||||||
std::array<std::uint32_t, 3> index {{2, 1, 0}};
|
|
||||||
triangleMesh.Build(verts, index, cmd);
|
|
||||||
|
|
||||||
std::array<RenderingElement3D, 2> renderers{};
|
|
||||||
for (std::uint32_t i = 0; i < renderers.size(); ++i) {
|
|
||||||
renderers[i].instance = {
|
|
||||||
.instanceCustomIndex = i,
|
|
||||||
.mask = 0xFF,
|
|
||||||
.instanceShaderBindingTableRecordOffset = 0,
|
|
||||||
.flags = VK_GEOMETRY_INSTANCE_FORCE_OPAQUE_BIT_KHR,
|
|
||||||
.accelerationStructureReference = triangleMesh.blasAddr,
|
|
||||||
};
|
|
||||||
MatrixRowMajor<float, 4, 3, 1>::Translation(i == 0 ? -100.0f : 100.0f, 0.0f, 0.0f)
|
|
||||||
.Store(reinterpret_cast<float*>(renderers[i].instance.transform.matrix));
|
|
||||||
RenderingElement3D::Add(&renderers[i]);
|
|
||||||
}
|
|
||||||
for (std::uint32_t f = 0; f < Window::numFrames; ++f) RenderingElement3D::BuildTLAS(cmd, f);
|
|
||||||
|
|
||||||
// Per descriptor set: a 2x2 texture whose green the hit shader reads, and
|
|
||||||
// a one-vec4 buffer whose red it reads.
|
|
||||||
std::array<Image2D<RGBA8>, 2> textures;
|
|
||||||
std::array<VulkanBuffer<float, true>, 2> colourBuffers;
|
|
||||||
for (std::size_t i = 0; i < textures.size(); ++i) {
|
|
||||||
textures[i].Create(2, 2, 1, cmd, VK_FORMAT_R8G8B8A8_UNORM,
|
|
||||||
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
|
|
||||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
|
||||||
for (std::size_t p = 0; p < 4; ++p) textures[i].buffer.value[p] = {0, textureGreen[i], 0, 255};
|
|
||||||
textures[i].Update(cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
|
||||||
|
|
||||||
colourBuffers[i].Create(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,
|
|
||||||
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, 4);
|
|
||||||
colourBuffers[i].value[0] = bufferRed[i];
|
|
||||||
colourBuffers[i].value[1] = 0.0f;
|
|
||||||
colourBuffers[i].value[2] = 0.0f;
|
|
||||||
colourBuffers[i].value[3] = 1.0f;
|
|
||||||
colourBuffers[i].FlushDevice();
|
|
||||||
}
|
|
||||||
|
|
||||||
window.FinishInit();
|
|
||||||
|
|
||||||
auto imgSlots = descriptorHeap.AllocateImageSlots(3);
|
|
||||||
auto bufSlots = descriptorHeap.AllocateBufferSlots(3);
|
|
||||||
auto samplerSlots = descriptorHeap.AllocateSamplerSlots(1);
|
|
||||||
|
|
||||||
std::array<VkImageViewCreateInfo, 2> textureViews{};
|
|
||||||
std::array<VkImageDescriptorInfoEXT, 2> textureInfos{};
|
|
||||||
std::array<VkDeviceAddressRangeKHR, 2> colourRanges{};
|
|
||||||
for (std::size_t i = 0; i < 2; ++i) {
|
|
||||||
textureViews[i] = {
|
|
||||||
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
|
|
||||||
.image = textures[i].image,
|
|
||||||
.viewType = VK_IMAGE_VIEW_TYPE_2D,
|
|
||||||
.format = VK_FORMAT_R8G8B8A8_UNORM,
|
|
||||||
.subresourceRange = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .baseMipLevel = 0,
|
|
||||||
.levelCount = 1, .baseArrayLayer = 0, .layerCount = 1 },
|
|
||||||
};
|
|
||||||
textureInfos[i] = {
|
|
||||||
.sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT,
|
|
||||||
.pView = &textureViews[i],
|
|
||||||
.layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
|
|
||||||
};
|
|
||||||
colourRanges[i] = { .address = colourBuffers[i].address, .size = 4 * sizeof(float) };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Written again whenever the swapchain is recreated, since the output
|
|
||||||
// image descriptors point at its images.
|
|
||||||
auto writeDescriptors = [&] {
|
|
||||||
std::array<VkDeviceAddressRangeKHR, Window::numFrames> tlasRanges{};
|
|
||||||
std::array<VkImageDescriptorInfoEXT, Window::numFrames> outInfos{};
|
|
||||||
std::vector<VkResourceDescriptorInfoEXT> resources;
|
|
||||||
std::vector<VkHostAddressRangeEXT> destinations;
|
|
||||||
auto add = [&](std::uint32_t f, VkResourceDescriptorInfoEXT resource, std::uint32_t offset, std::uint32_t size) {
|
|
||||||
resources.push_back(resource);
|
|
||||||
destinations.push_back({ .address = descriptorHeap.resourceHeap[f].value + offset, .size = size });
|
|
||||||
};
|
|
||||||
const std::uint32_t imageSize = Device::descriptorHeapProperties.imageDescriptorSize;
|
|
||||||
const std::uint32_t bufferSize = Device::descriptorHeapProperties.bufferDescriptorSize;
|
|
||||||
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
|
||||||
tlasRanges[f] = { .address = RenderingElement3D::tlases[f].address };
|
|
||||||
outInfos[f] = {
|
|
||||||
.sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT,
|
|
||||||
.pView = &window.imageViews[f],
|
|
||||||
.layout = VK_IMAGE_LAYOUT_GENERAL,
|
|
||||||
};
|
|
||||||
add(f, { .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
|
||||||
.type = VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR,
|
|
||||||
.data = { .pAddressRange = &tlasRanges[f] } },
|
|
||||||
descriptorHeap.BufferByteOffset(bufSlots.firstElement), bufferSize);
|
|
||||||
add(f, { .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
|
||||||
.type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
|
|
||||||
.data = { .pImage = &outInfos[f] } },
|
|
||||||
descriptorHeap.ImageByteOffset(imgSlots.firstElement), imageSize);
|
|
||||||
for (std::uint16_t i = 0; i < 2; ++i) {
|
|
||||||
add(f, { .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
|
||||||
.type = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
|
|
||||||
.data = { .pImage = &textureInfos[i] } },
|
|
||||||
descriptorHeap.ImageByteOffset(imgSlots.firstElement + 1 + i), imageSize);
|
|
||||||
add(f, { .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
|
|
||||||
.type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
|
|
||||||
.data = { .pAddressRange = &colourRanges[i] } },
|
|
||||||
descriptorHeap.BufferByteOffset(bufSlots.firstElement + 1 + i), bufferSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Device::vkWriteResourceDescriptorsEXT(Device::device, static_cast<std::uint32_t>(resources.size()),
|
|
||||||
resources.data(), destinations.data());
|
|
||||||
for (auto& heap : descriptorHeap.resourceHeap) heap.FlushDevice();
|
|
||||||
};
|
|
||||||
writeDescriptors();
|
|
||||||
EventListener<void> resizeListener(&window.onResize, writeDescriptors);
|
|
||||||
|
|
||||||
VkSamplerCreateInfo samplerInfo = {
|
|
||||||
.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
|
|
||||||
.magFilter = VK_FILTER_NEAREST,
|
|
||||||
.minFilter = VK_FILTER_NEAREST,
|
|
||||||
.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST,
|
|
||||||
.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
|
|
||||||
.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
|
|
||||||
.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
|
|
||||||
.maxLod = VK_LOD_CLAMP_NONE,
|
|
||||||
};
|
|
||||||
std::array<VkSamplerCreateInfo, Window::numFrames> samplerInfos{};
|
|
||||||
std::array<VkHostAddressRangeEXT, Window::numFrames> samplerDestinations{};
|
|
||||||
for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
|
|
||||||
samplerInfos[f] = samplerInfo;
|
|
||||||
samplerDestinations[f] = {
|
|
||||||
.address = descriptorHeap.samplerHeap[f].value + descriptorHeap.SamplerByteOffset(samplerSlots.firstElement),
|
|
||||||
.size = Device::descriptorHeapProperties.samplerDescriptorSize,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Device::vkWriteSamplerDescriptorsEXT(Device::device, Window::numFrames, samplerInfos.data(), samplerDestinations.data());
|
|
||||||
for (auto& heap : descriptorHeap.samplerHeap) heap.FlushDevice();
|
|
||||||
|
|
||||||
window.descriptorHeap = &descriptorHeap;
|
|
||||||
RTPass rtPass(&pipeline);
|
|
||||||
window.passes.push_back(&rtPass);
|
|
||||||
|
|
||||||
// More frames than are in flight, so every per-frame heap copy is read at
|
|
||||||
// least once. A device loss here throws out of Render().
|
|
||||||
for (int i = 0; i < Window::numFrames * 2; ++i) window.Render();
|
|
||||||
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
|
||||||
|
|
||||||
// Counted before the capture: SaveFrame reads back an image that has
|
|
||||||
// already been handed to the presentation engine, which the validation
|
|
||||||
// layer reports on its own.
|
|
||||||
if (std::getenv("CRAFTER_GRAPHICS_VALIDATION") != nullptr) {
|
|
||||||
Check(Device::validationErrorCount == 0,
|
|
||||||
std::format("no Vulkan validation errors while rendering ({} seen)", Device::validationErrorCount));
|
|
||||||
}
|
|
||||||
|
|
||||||
window.SaveFrame(out);
|
|
||||||
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
|
||||||
const Frame frame = LoadFrame(out);
|
|
||||||
Check(!frame.rgba.empty(), "the frame was read back");
|
|
||||||
if (!frame.rgba.empty()) {
|
|
||||||
const int y = frame.height / 2;
|
|
||||||
// An even column then the odd one next to it, in each triangle.
|
|
||||||
const int left = FrameX(-100.0f, frame) & ~1;
|
|
||||||
const int right = FrameX(100.0f, frame) & ~1;
|
|
||||||
CheckPixel(frame, left, y, setColour[0], "instance 0, even column (set 0)");
|
|
||||||
CheckPixel(frame, left + 1, y, setColour[0], "instance 0, odd column (set 0)");
|
|
||||||
CheckPixel(frame, right, y, setColour[1], "instance 1, even column (set 1)");
|
|
||||||
CheckPixel(frame, right + 1, y, setColour[0], "instance 1, odd column (set 0)");
|
|
||||||
CheckPixel(frame, frame.width / 2, y, {0, 0, 255}, "miss between the triangles");
|
|
||||||
CheckPixel(frame, frame.width / 20, frame.height / 10, {0, 0, 255}, "miss in the corner");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (RenderingElement3D& renderer : renderers) RenderingElement3D::Remove(&renderer);
|
|
||||||
return failures - failuresBefore;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A fresh directory for the read-back frames, so a frame left by an earlier
|
|
||||||
// run can't stand in for one this run failed to write.
|
|
||||||
fs::path OutputDir() {
|
|
||||||
const fs::path dir = fs::temp_directory_path() / "crafter-heap-dynamic-index-hit";
|
|
||||||
std::error_code ec;
|
|
||||||
fs::remove_all(dir, ec);
|
|
||||||
fs::create_directories(dir);
|
|
||||||
return dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
int main(int argc, char** argv) {
|
|
||||||
Device::Initialize();
|
|
||||||
if (!Device::rayTracingSupported) {
|
|
||||||
std::println("SKIP no Vulkan ray-tracing device");
|
|
||||||
return 77;
|
|
||||||
}
|
|
||||||
if (Device::driverProperties.driverID == VK_DRIVER_ID_NVIDIA_PROPRIETARY
|
|
||||||
&& NvidiaVersion() < std::tuple(615u, 78u, 8u)) {
|
|
||||||
const auto [major, minor, patch] = NvidiaVersion();
|
|
||||||
std::println("SKIP NVIDIA {}.{:02}.{:02} loses the device on this (fixed in 615.78.08)", major, minor, patch);
|
|
||||||
return 77;
|
|
||||||
}
|
|
||||||
|
|
||||||
// `--render <png>`: the child run, rendering as configured.
|
|
||||||
if (argc == 3 && std::string_view(argv[1]) == "--render") {
|
|
||||||
RenderScene(argv[2]);
|
|
||||||
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fs::path dir = OutputDir();
|
|
||||||
if (std::getenv("CRAFTER_GRAPHICS_VALIDATION") != nullptr) {
|
|
||||||
const fs::path exe = fs::read_symlink("/proc/self/exe");
|
|
||||||
const int exit = RunCommand(std::format("env -u CRAFTER_GRAPHICS_VALIDATION '{}' --render '{}'",
|
|
||||||
exe.string(), (dir / "unvalidated.png").string()));
|
|
||||||
Check(exit == 0, std::format("the scene renders with validation off (exit {})", exit));
|
|
||||||
}
|
|
||||||
RenderScene(dir / "frame.png");
|
|
||||||
|
|
||||||
std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed");
|
|
||||||
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
|
|
||||||
}
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
//SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
// The dependency RenderingElement3D::BuildTLAS records after its build, which
|
|
||||||
// orders the build before the shaders that read the TLAS: traceRayEXT in a
|
|
||||||
// ray-tracing pass, rayQueryEXT in a compute pass, recorded after it in the
|
|
||||||
// same command buffer (BuildTLAS in onUpdate, then RTPass, is the documented
|
|
||||||
// per-frame flow). It used to be an execution-only barrier with no memory
|
|
||||||
// barrier, which orders the commands but never makes the build's
|
|
||||||
// acceleration-structure writes available and visible to those reads. The
|
|
||||||
// dependency is built by BuildTlasReadBarrier, pure CPU logic that this test
|
|
||||||
// drives directly with no GPU device, like SwapchainBarrierScope.
|
|
||||||
|
|
||||||
#include <cstdlib>
|
|
||||||
#include "vulkan/vulkan.h"
|
|
||||||
|
|
||||||
import Crafter.Graphics;
|
|
||||||
import std;
|
|
||||||
|
|
||||||
using namespace Crafter;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
int failures = 0;
|
|
||||||
|
|
||||||
void Check(bool ok, std::string_view what) {
|
|
||||||
std::println("{} {}", ok ? "PASS" : "FAIL", what);
|
|
||||||
if (!ok) ++failures;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
const TlasReadBarrier barrier = BuildTlasReadBarrier();
|
|
||||||
|
|
||||||
Check(barrier.srcStage == VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_KHR,
|
|
||||||
"the dependency starts at the acceleration-structure build");
|
|
||||||
Check((barrier.dstStage & VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR) != 0,
|
|
||||||
"it reaches ray-tracing shaders (traceRayEXT)");
|
|
||||||
Check((barrier.dstStage & VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT) != 0,
|
|
||||||
"it reaches compute shaders (rayQueryEXT)");
|
|
||||||
|
|
||||||
Check(barrier.memory.sType == VK_STRUCTURE_TYPE_MEMORY_BARRIER, "it carries a memory barrier");
|
|
||||||
Check((barrier.memory.srcAccessMask & VK_ACCESS_ACCELERATION_STRUCTURE_WRITE_BIT_KHR) != 0,
|
|
||||||
"the build's acceleration-structure writes are made available");
|
|
||||||
Check((barrier.memory.dstAccessMask & VK_ACCESS_ACCELERATION_STRUCTURE_READ_BIT_KHR) != 0,
|
|
||||||
"and visible to acceleration-structure reads");
|
|
||||||
|
|
||||||
std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed");
|
|
||||||
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
|
|
||||||
}
|
|
||||||
|
|
@ -1,133 +0,0 @@
|
||||||
//SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
// RenderingElement3D::BuildTLAS with no elements registered, i.e. an app that
|
|
||||||
// builds its TLAS before the scene has anything in it. The TLAS instance and
|
|
||||||
// metadata buffers used to be sized to the instance count, so the first build
|
|
||||||
// of an empty scene created zero-size buffers: invalid usage, which NVIDIA
|
|
||||||
// answers with VK_ERROR_OUT_OF_DEVICE_MEMORY, so BuildTLAS threw. An empty
|
|
||||||
// TLAS is valid to trace against (every ray misses), so it has to build, and
|
|
||||||
// adding and removing instances afterwards has to keep working.
|
|
||||||
//
|
|
||||||
// Needs a Vulkan RT device, like BLASBuildOptions; without RT it skips.
|
|
||||||
|
|
||||||
#include <cstdlib>
|
|
||||||
#include "vulkan/vulkan.h"
|
|
||||||
|
|
||||||
import Crafter.Graphics;
|
|
||||||
import Crafter.Math;
|
|
||||||
import std;
|
|
||||||
|
|
||||||
using namespace Crafter;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
int failures = 0;
|
|
||||||
|
|
||||||
void Check(bool ok, std::string_view what) {
|
|
||||||
std::println("{} {}", ok ? "PASS" : "FAIL", what);
|
|
||||||
if (!ok) ++failures;
|
|
||||||
}
|
|
||||||
|
|
||||||
// One-time command buffer helpers: record a build, submit, block.
|
|
||||||
VkCommandBuffer BeginCmd() {
|
|
||||||
VkCommandBufferAllocateInfo allocInfo {
|
|
||||||
.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, &allocInfo, &cmd));
|
|
||||||
VkCommandBufferBeginInfo beginInfo {
|
|
||||||
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
|
|
||||||
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
|
|
||||||
};
|
|
||||||
Device::CheckVkResult(vkBeginCommandBuffer(cmd, &beginInfo));
|
|
||||||
return cmd;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SubmitWait(VkCommandBuffer cmd) {
|
|
||||||
Device::CheckVkResult(vkEndCommandBuffer(cmd));
|
|
||||||
VkSubmitInfo submitInfo {
|
|
||||||
.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
|
|
||||||
.commandBufferCount = 1,
|
|
||||||
.pCommandBuffers = &cmd,
|
|
||||||
};
|
|
||||||
Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &submitInfo, VK_NULL_HANDLE));
|
|
||||||
Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
|
|
||||||
vkFreeCommandBuffers(Device::device, Device::commandPool, 1, &cmd);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Builds frame 0's TLAS from whatever is registered; false if it threw.
|
|
||||||
bool BuildFrame0() {
|
|
||||||
VkCommandBuffer cmd = BeginCmd();
|
|
||||||
try {
|
|
||||||
RenderingElement3D::BuildTLAS(cmd, 0);
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
std::println(" BuildTLAS threw: {}", e.what());
|
|
||||||
vkEndCommandBuffer(cmd);
|
|
||||||
vkFreeCommandBuffers(Device::device, Device::commandPool, 1, &cmd);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
SubmitWait(cmd);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
Device::Initialize();
|
|
||||||
if (!Device::rayTracingSupported) {
|
|
||||||
std::println("SKIP no Vulkan ray-tracing device");
|
|
||||||
return 77;
|
|
||||||
}
|
|
||||||
Device::validationErrorCount = 0;
|
|
||||||
const TlasWithBuffer& tlas = RenderingElement3D::tlases[0];
|
|
||||||
|
|
||||||
Check(RenderingElement3D::elements.empty(), "the scene starts empty");
|
|
||||||
if (!BuildFrame0()) {
|
|
||||||
Check(false, "an empty scene's TLAS builds");
|
|
||||||
std::println("check(s) failed");
|
|
||||||
return EXIT_FAILURE;
|
|
||||||
}
|
|
||||||
Check(true, "an empty scene's TLAS builds");
|
|
||||||
Check(tlas.accelerationStructure != VK_NULL_HANDLE && tlas.address != 0,
|
|
||||||
"it is a real acceleration structure with a device address");
|
|
||||||
Check(tlas.builtInstanceCount == 0, "built for zero instances");
|
|
||||||
|
|
||||||
// One instance added to the scene afterwards.
|
|
||||||
Mesh triangle;
|
|
||||||
{
|
|
||||||
VkCommandBuffer cmd = BeginCmd();
|
|
||||||
std::array<Vector<float, 3, 3>, 3> verts {{{-1, -1, 0}, {0, 1, 0}, {1, -1, 0}}};
|
|
||||||
std::array<std::uint32_t, 3> index {{2, 1, 0}};
|
|
||||||
triangle.Build(verts, index, cmd);
|
|
||||||
SubmitWait(cmd);
|
|
||||||
}
|
|
||||||
RenderingElement3D element;
|
|
||||||
element.instance = {
|
|
||||||
.instanceCustomIndex = 0,
|
|
||||||
.mask = 0xFF,
|
|
||||||
.instanceShaderBindingTableRecordOffset = 0,
|
|
||||||
.flags = VK_GEOMETRY_INSTANCE_FORCE_OPAQUE_BIT_KHR,
|
|
||||||
.accelerationStructureReference = triangle.blasAddr,
|
|
||||||
};
|
|
||||||
MatrixRowMajor<float, 4, 3, 1>::Identity().Store(reinterpret_cast<float*>(element.instance.transform.matrix));
|
|
||||||
RenderingElement3D::Add(&element);
|
|
||||||
Check(BuildFrame0(), "the TLAS rebuilds once an instance is added");
|
|
||||||
Check(tlas.builtInstanceCount == 1, "built for one instance");
|
|
||||||
|
|
||||||
// And back to empty.
|
|
||||||
RenderingElement3D::Remove(&element);
|
|
||||||
Check(BuildFrame0(), "the TLAS rebuilds once the scene is empty again");
|
|
||||||
Check(tlas.builtInstanceCount == 0, "built for zero instances again");
|
|
||||||
|
|
||||||
if (std::getenv("CRAFTER_GRAPHICS_VALIDATION") != nullptr) {
|
|
||||||
Check(Device::validationErrorCount == 0,
|
|
||||||
std::format("no Vulkan validation errors ({} seen)", Device::validationErrorCount));
|
|
||||||
}
|
|
||||||
|
|
||||||
std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed");
|
|
||||||
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
|
|
||||||
}
|
|
||||||
|
|
@ -351,6 +351,22 @@ int main() {
|
||||||
Check(Device::validationErrorCount == 0,
|
Check(Device::validationErrorCount == 0,
|
||||||
std::format("no Vulkan validation errors ({} seen)", Device::validationErrorCount));
|
std::format("no Vulkan validation errors ({} seen)", Device::validationErrorCount));
|
||||||
|
|
||||||
|
// Tear down the frame-0 TLAS while the Vulkan device is still alive. The
|
||||||
|
// static tlases[] array is destroyed at process exit, by which point the
|
||||||
|
// device (also static) may already be gone — so its VulkanBuffer
|
||||||
|
// destructors would fault. Releasing here keeps the exit path clean.
|
||||||
|
{
|
||||||
|
auto& t0 = RenderingElement3D::tlases[0];
|
||||||
|
if (t0.accelerationStructure != VK_NULL_HANDLE) {
|
||||||
|
Device::vkDestroyAccelerationStructureKHR(Device::device, t0.accelerationStructure, nullptr);
|
||||||
|
t0.accelerationStructure = VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
if (t0.instanceBuffer.buffer != VK_NULL_HANDLE) t0.instanceBuffer.Clear();
|
||||||
|
if (t0.metadataBuffer.buffer != VK_NULL_HANDLE) t0.metadataBuffer.Clear();
|
||||||
|
if (t0.scratchBuffer.buffer != VK_NULL_HANDLE) t0.scratchBuffer.Clear();
|
||||||
|
if (t0.buffer.buffer != VK_NULL_HANDLE) t0.buffer.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
if (failures != 0) {
|
if (failures != 0) {
|
||||||
std::println("{} check(s) failed", failures);
|
std::println("{} check(s) failed", failures);
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
|
|
|
||||||
|
|
@ -213,6 +213,20 @@ int main() {
|
||||||
Check(Device::validationErrorCount == 0,
|
Check(Device::validationErrorCount == 0,
|
||||||
std::format("no Vulkan validation errors ({} seen)", Device::validationErrorCount));
|
std::format("no Vulkan validation errors ({} seen)", Device::validationErrorCount));
|
||||||
|
|
||||||
|
// Tear down the frame-0 TLAS while the device is still alive (same rationale
|
||||||
|
// as TLASHighWaterMark — the static tlases[] outlive the static device).
|
||||||
|
{
|
||||||
|
auto& t0 = RenderingElement3D::tlases[0];
|
||||||
|
if (t0.accelerationStructure != VK_NULL_HANDLE) {
|
||||||
|
Device::vkDestroyAccelerationStructureKHR(Device::device, t0.accelerationStructure, nullptr);
|
||||||
|
t0.accelerationStructure = VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
if (t0.instanceBuffer.buffer != VK_NULL_HANDLE) t0.instanceBuffer.Clear();
|
||||||
|
if (t0.metadataBuffer.buffer != VK_NULL_HANDLE) t0.metadataBuffer.Clear();
|
||||||
|
if (t0.scratchBuffer.buffer != VK_NULL_HANDLE) t0.scratchBuffer.Clear();
|
||||||
|
if (t0.buffer.buffer != VK_NULL_HANDLE) t0.buffer.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
if (failures != 0) {
|
if (failures != 0) {
|
||||||
std::println("{} check(s) failed", failures);
|
std::println("{} check(s) failed", failures);
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue