Adding physics.

This commit is contained in:
Scott Duensing 2026-09-05 19:52:04 -05:00
parent 3fdee3d9b6
commit 807e61a887
586 changed files with 118826 additions and 4 deletions

12
.gitattributes vendored
View file

@ -26,9 +26,16 @@
*.exe filter=lfs diff=lfs merge=lfs -text
*.dll filter=lfs diff=lfs merge=lfs -text
*.dat filter=lfs diff=lfs merge=lfs -text
#
# Singe game and menu descriptors are Lua text, not binary.
#
games.dat !filter !diff !merge text
menu.dat !filter !diff !merge text
#
# Fonts
#
*.ttf filter=lfs diff=lfs merge=lfs -text
#
@ -75,3 +82,8 @@ menu.dat !filter !diff !merge text
*.lzma filter=lfs diff=lfs merge=lfs -text
*.gz filter=lfs diff=lfs merge=lfs -text
*.tar filter=lfs diff=lfs merge=lfs -text
#
# Model
#
*.glb filter=lfs diff=lfs merge=lfs -text

View file

@ -20,7 +20,7 @@
cmake_minimum_required(VERSION 3.22)
project(singe2 VERSION 3.00 LANGUAGES C)
project(singe2 VERSION 3.00 LANGUAGES C CXX)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
@ -273,6 +273,8 @@ set(SINGE_SOURCE
src/main.h
src/pack.c
src/pack.h
src/physics.h
src/physicsJolt.cpp
src/model.c
src/model.h
src/math3d.c
@ -622,12 +624,25 @@ foreach(imageLib libjpeg.a libpng16.a)
endif()
endforeach()
# Jolt Physics comes with the compile definitions it was built with (SIMD level, layer bits) through
# its exported target; the one C++ file in the tree (physicsJolt.cpp) needs them. Its C++ runtime is
# zig's own libc++, linked statically, or the host's libstdc++ under gcc; macOS already lists -lc++.
find_package(Jolt CONFIG REQUIRED PATHS ${BUILD_DIR}/lib/cmake/Jolt NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH)
set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF)
if(NOT KANGAROO_OS STREQUAL "macos")
if(SINGE_ZIG_TARGET)
list(APPEND SYSTEM_LIBS -lc++)
else()
list(APPEND SYSTEM_LIBS -lstdc++)
endif()
endif()
# System libraries follow the static ones so their symbols resolve for everything above.
# Apple's linker has no --start-group, so the list is repeated there instead.
if(KANGAROO_OS STREQUAL "macos")
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE ${STATIC_LIBS} ${STATIC_LIBS} ${STATIC_LIBS} ${SYSTEM_LIBS} -pthread -lm)
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE ${STATIC_LIBS} ${STATIC_LIBS} ${STATIC_LIBS} Jolt::Jolt ${SYSTEM_LIBS} -pthread -lm)
else()
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE -Wl,--start-group ${STATIC_LIBS} -Wl,--end-group ${SYSTEM_LIBS} -pthread -lm)
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE -Wl,--start-group ${STATIC_LIBS} -Wl,--end-group Jolt::Jolt ${SYSTEM_LIBS} -pthread -lm)
endif()

View file

@ -9,6 +9,7 @@ cgltf MIT https://github.com/jkuhlmann/cgltf
copas MIT https://lunarmodules.github.io/copas
ffmpeg LGPL-2.1 https://ffmpeg.org
freetype FTL https://freetype.org
JoltPhysics MIT https://github.com/jrouwe/JoltPhysics
json.lua MIT https://github.com/rxi/json.lua
libjpeg-turbo IJG https://libjpeg-turbo.org
libogg BSD-3-Clause https://xiph.org/ogg

View file

@ -169,6 +169,22 @@ singeCmakeProject(zstd ${SB_THIRDPARTY}/zstd/build/cmake "" "-DZSTD_BUILD_SHARED
singeCmakeProject(SDL3 ${SB_THIRDPARTY}/SDL3 "" "-DSDL_SHARED=off;-DSDL_STATIC=on;-DSDL_TESTS=off;-DSDL_EXAMPLES=off" "${SB_ENV}")
# ===== Jolt Physics (C++; static, single precision, no tests or samples) =====
# The x86 baseline is SSE4.1/4.2 so a shipped binary runs on anything from 2008 on; AVX and the
# bit-count instructions would silently raise it. No link-time optimisation through the cross
# compiler, no warnings-as-errors with a clang Jolt has not met, nothing but the library; the GPU
# compute paths (hair and soft bodies on DX12, Vulkan, Metal or a CPU fallback) would need a shader
# compiler at build time and are not used.
set(joltOptions
-DTARGET_UNIT_TESTS=OFF -DTARGET_HELLO_WORLD=OFF -DTARGET_PERFORMANCE_TEST=OFF -DTARGET_SAMPLES=OFF -DTARGET_VIEWER=OFF
-DJPH_BUILD_SHARED_LIBS=OFF -DDOUBLE_PRECISION=OFF -DCROSS_PLATFORM_DETERMINISTIC=OFF
-DUSE_SSE4_1=ON -DUSE_SSE4_2=ON -DUSE_AVX=OFF -DUSE_AVX2=OFF -DUSE_AVX512=OFF -DUSE_F16C=OFF -DUSE_FMADD=OFF -DUSE_LZCNT=OFF -DUSE_TZCNT=OFF
-DINTERPROCEDURAL_OPTIMIZATION=OFF -DENABLE_ALL_WARNINGS=OFF -DGENERATE_DEBUG_SYMBOLS=OFF
-DDEBUG_RENDERER_IN_DEBUG_AND_RELEASE=OFF -DFLOATING_POINT_EXCEPTIONS_ENABLED=OFF -DCPP_EXCEPTIONS_ENABLED=OFF -DCPP_RTTI_ENABLED=OFF
-DJPH_USE_DX12=OFF -DJPH_USE_VK=OFF -DJPH_USE_MTL=OFF -DJPH_USE_CPU_COMPUTE=OFF
)
singeCmakeProject(Jolt ${SB_THIRDPARTY}/JoltPhysics/Build "" "${joltOptions}" "${SB_ENV}")
set(sdl3Dir -DSDL3_DIR=${SB_PREFIX}/lib/cmake/SDL3)
singeCmakeProject(SDL3_image ${SB_THIRDPARTY}/SDL3_image "SDL3;zlib"
@ -299,7 +315,7 @@ set(singeBinary ${SB_PREFIX}/singe)
ExternalProject_Add(singe
SOURCE_DIR ${CMAKE_SOURCE_DIR}
BINARY_DIR ${singeBinary}
DEPENDS zlib zstd SDL3 SDL3_image SDL3_mixer SDL3_ttf openssl ffmpeg
DEPENDS zlib zstd SDL3 SDL3_image SDL3_mixer SDL3_ttf openssl ffmpeg Jolt
CONFIGURE_COMMAND ${SB_ENV} ${CMAKE_COMMAND} -S ${CMAKE_SOURCE_DIR} -B ${singeBinary} ${SB_CMAKE_ARGS} -DSINGE_SUPERBUILD=OFF -DKANGAROO_OS=${KANGAROO_OS} -DKANGAROO_ARCH=${KANGAROO_ARCH} -DSINGE_TREE=${SINGE_TREE}
BUILD_COMMAND ${SB_ENV} ${CMAKE_COMMAND} --build ${singeBinary} --parallel ${SB_JOBS}
INSTALL_COMMAND ${CMAKE_COMMAND} -E copy ${singeBinary}/${singeBinaryBase}${CMAKE_EXECUTABLE_SUFFIX} ${CMAKE_SOURCE_DIR}/.builddir/${singeBinaryName}

81
src/physics.h Normal file
View file

@ -0,0 +1,81 @@
/*
*
* Singe 3
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*
*/
#ifndef PHYSICS_H
#define PHYSICS_H
#include <stdbool.h>
#include <stdint.h>
#include "math3d.h"
#ifdef __cplusplus
extern "C" {
#endif
// The physics world (Jolt Physics behind this C interface; see physicsJolt.cpp). A body lives on
// a scene node: static bodies stay where the node was when they were made, kinematic bodies follow
// their node each frame, dynamic bodies drive their node.
typedef enum BodyTypeE {
BODY_STATIC = 0,
BODY_DYNAMIC = 1,
BODY_KINEMATIC = 2
} BodyTypeE;
typedef enum ShapeTypeE {
SHAPE_BOX = 0, // width, height, depth
SHAPE_SPHERE = 1, // radius
SHAPE_CAPSULE = 2, // radius, height (caps included), along Y
SHAPE_CYLINDER = 3, // radius, height, along Y
SHAPE_HULL = 4, // a convex hull of the node's mesh (stage 4)
SHAPE_MESH = 5 // the node's mesh triangles, static only (stage 4)
} ShapeTypeE;
bool bodyApplyForce(int32_t node, Vec3T force, const Vec3T *at);
bool bodyApplyImpulse(int32_t node, Vec3T impulse, const Vec3T *at);
bool bodyDelete(int32_t node);
bool bodyExists(int32_t node);
Vec3T bodyGetAngularVelocity(int32_t node);
Vec3T bodyGetVelocity(int32_t node);
bool bodyIsResting(int32_t node);
bool bodyNew(int32_t node, BodyTypeE type, ShapeTypeE shape, float a, float b, float c);
bool bodySetAngularVelocity(int32_t node, Vec3T velocity);
bool bodySetBounce(int32_t node, float bounce);
bool bodySetEnabled(int32_t node, bool enabled);
bool bodySetFriction(int32_t node, float friction);
bool bodySetMass(int32_t node, float kilograms);
bool bodySetVelocity(int32_t node, Vec3T velocity);
bool physicsAvailable(void);
bool physicsInit(void);
void physicsQuit(void);
void physicsSetEnabled(bool enabled);
void physicsSetGravity(Vec3T gravity);
void physicsUpdate(bool advance);
#ifdef __cplusplus
}
#endif
#endif

596
src/physicsJolt.cpp Normal file
View file

@ -0,0 +1,596 @@
/*
*
* Singe 3
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*
*/
// Jolt Physics behind the C interface in physics.h: the only C++ in Singe. Nothing of Jolt's
// crosses the header; the engine sees plain C functions and node handles.
#include <SDL3/SDL.h>
#include <Jolt/Jolt.h>
#include <Jolt/RegisterTypes.h>
#include <Jolt/Core/Factory.h>
#include <Jolt/Core/TempAllocator.h>
#include <Jolt/Core/JobSystemThreadPool.h>
#include <Jolt/Physics/PhysicsSettings.h>
#include <Jolt/Physics/PhysicsSystem.h>
#include <Jolt/Physics/Body/BodyCreationSettings.h>
#include <Jolt/Physics/Body/BodyLock.h>
#include <Jolt/Physics/Collision/BroadPhase/BroadPhaseLayer.h>
#include <Jolt/Physics/Collision/ObjectLayer.h>
#include <Jolt/Physics/Collision/Shape/BoxShape.h>
#include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
#include <Jolt/Physics/Collision/Shape/CylinderShape.h>
#include <Jolt/Physics/Collision/Shape/SphereShape.h>
extern "C" {
#include "util.h"
#include "scene.h"
}
#include "physics.h"
#define MAX_BODIES 4096
#define MAX_BODY_PAIRS 4096
#define MAX_CONTACTS 8192
#define TEMP_ALLOCATOR_BYTES (16 * 1024 * 1024)
#define MIN_JOB_THREADS 1
#define STEP_SECONDS (1.0 / 60.0)
#define MAX_STEPS_PER_FRAME 4
#define MIN_DIMENSION 0.001f
#define DEFAULT_FRICTION 0.5f
#define DEFAULT_BOUNCE 0.1f
#define NO_HANDLE -1
// Two object layers: what never moves and what may. Static bodies never collide with each other.
namespace {
const JPH::ObjectLayer LAYER_NON_MOVING = 0;
const JPH::ObjectLayer LAYER_MOVING = 1;
const JPH::BroadPhaseLayer BROAD_NON_MOVING(0);
const JPH::BroadPhaseLayer BROAD_MOVING(1);
const JPH::uint BROAD_COUNT = 2;
class ObjectPairFilterT final : public JPH::ObjectLayerPairFilter {
public:
bool ShouldCollide(JPH::ObjectLayer a, JPH::ObjectLayer b) const override {
return (a == LAYER_MOVING) || (b == LAYER_MOVING);
}
};
class BroadPhaseLayersT final : public JPH::BroadPhaseLayerInterface {
public:
JPH::uint GetNumBroadPhaseLayers() const override {
return BROAD_COUNT;
}
JPH::BroadPhaseLayer GetBroadPhaseLayer(JPH::ObjectLayer layer) const override {
return (layer == LAYER_NON_MOVING) ? BROAD_NON_MOVING : BROAD_MOVING;
}
#if defined(JPH_EXTERNAL_PROFILE) || defined(JPH_PROFILE_ENABLED)
const char *GetBroadPhaseLayerName(JPH::BroadPhaseLayer layer) const override {
return (layer == BROAD_NON_MOVING) ? "NON_MOVING" : "MOVING";
}
#endif
};
class ObjectVsBroadPhaseFilterT final : public JPH::ObjectVsBroadPhaseLayerFilter {
public:
bool ShouldCollide(JPH::ObjectLayer layer, JPH::BroadPhaseLayer broad) const override {
return (layer == LAYER_MOVING) || (broad == BROAD_MOVING);
}
};
// A body on a scene node.
struct BodyRecordT {
int32_t node;
uint32_t generation; // The node's, so a reused handle is not mistaken for this body
JPH::BodyID id;
BodyTypeE type;
bool enabled; // In the world (bodySetEnabled)
bool used;
};
struct WorldT {
JPH::TempAllocatorImpl *tempAllocator;
JPH::JobSystemThreadPool *jobs;
BroadPhaseLayersT broadPhaseLayers;
ObjectVsBroadPhaseFilterT objectVsBroadPhase;
ObjectPairFilterT objectPairs;
JPH::PhysicsSystem *system;
BodyRecordT *bodies;
int32_t bodyCount;
double accumulator; // Seconds owed to the fixed step
uint64_t lastTick;
bool enabled;
};
WorldT *_world = nullptr;
BodyRecordT *_find(int32_t node);
JPH::Quat _fromQuat(QuatT q);
JPH::Vec3 _fromVec3(Vec3T v);
void _release(BodyRecordT *record);
QuatT _toQuat(JPH::Quat q);
Vec3T _toVec3(JPH::Vec3 v);
void _trace(const char *fmt, ...);
// The record for a node's body, or NULL. A record whose node was deleted (or reused) is
// released on the way.
BodyRecordT *_find(int32_t node) {
int32_t x;
if (_world == nullptr) {
return nullptr;
}
for (x = 0; x < _world->bodyCount; x++) {
BodyRecordT *record = &_world->bodies[x];
if (!record->used || (record->node != node)) {
continue;
}
if (!nodeValid(node) || (nodeGetGeneration(node) != record->generation)) {
_release(record);
return nullptr;
}
return record;
}
return nullptr;
}
JPH::Quat _fromQuat(QuatT q) {
return JPH::Quat(q.x, q.y, q.z, q.w).Normalized();
}
JPH::Vec3 _fromVec3(Vec3T v) {
return JPH::Vec3(v.x, v.y, v.z);
}
// Takes the body out of the world and frees its slot.
void _release(BodyRecordT *record) {
JPH::BodyInterface &bodies = _world->system->GetBodyInterface();
if (record->enabled) {
bodies.RemoveBody(record->id);
}
bodies.DestroyBody(record->id);
memset(record, 0, sizeof(*record));
}
QuatT _toQuat(JPH::Quat q) {
QuatT out = { q.GetX(), q.GetY(), q.GetZ(), q.GetW() };
return out;
}
Vec3T _toVec3(JPH::Vec3 v) {
return vec3(v.GetX(), v.GetY(), v.GetZ());
}
// Jolt's trace goes to Singe's.
void _trace(const char *fmt, ...) {
char buffer[1024];
va_list args;
va_start(args, fmt);
vsnprintf(buffer, sizeof(buffer), fmt, args);
va_end(args);
utilTrace("Physics: %s", buffer);
}
}
// ===== Bodies =====
// A force for this step, at the centre of mass or a world point.
bool bodyApplyForce(int32_t node, Vec3T force, const Vec3T *at) {
BodyRecordT *record = _find(node);
if ((record == nullptr) || (record->type != BODY_DYNAMIC)) {
return false;
}
if (at != nullptr) {
_world->system->GetBodyInterface().AddForce(record->id, _fromVec3(force), JPH::RVec3(at->x, at->y, at->z));
} else {
_world->system->GetBodyInterface().AddForce(record->id, _fromVec3(force));
}
return true;
}
// An instant change of momentum, at the centre of mass or a world point.
bool bodyApplyImpulse(int32_t node, Vec3T impulse, const Vec3T *at) {
BodyRecordT *record = _find(node);
if ((record == nullptr) || (record->type != BODY_DYNAMIC)) {
return false;
}
if (at != nullptr) {
_world->system->GetBodyInterface().AddImpulse(record->id, _fromVec3(impulse), JPH::RVec3(at->x, at->y, at->z));
} else {
_world->system->GetBodyInterface().AddImpulse(record->id, _fromVec3(impulse));
}
return true;
}
bool bodyDelete(int32_t node) {
BodyRecordT *record = _find(node);
if (record == nullptr) {
return false;
}
_release(record);
return true;
}
bool bodyExists(int32_t node) {
return _find(node) != nullptr;
}
Vec3T bodyGetAngularVelocity(int32_t node) {
BodyRecordT *record = _find(node);
if (record == nullptr) {
return vec3(0.0f, 0.0f, 0.0f);
}
return _toVec3(_world->system->GetBodyInterface().GetAngularVelocity(record->id));
}
Vec3T bodyGetVelocity(int32_t node) {
BodyRecordT *record = _find(node);
if (record == nullptr) {
return vec3(0.0f, 0.0f, 0.0f);
}
return _toVec3(_world->system->GetBodyInterface().GetLinearVelocity(record->id));
}
// Asleep: a dynamic body that has come to rest (static and disabled bodies count as resting).
bool bodyIsResting(int32_t node) {
BodyRecordT *record = _find(node);
if (record == nullptr) {
return false;
}
return !record->enabled || !_world->system->GetBodyInterface().IsActive(record->id);
}
// Gives the node a body with a shape sized by a, b, c (see ShapeTypeE) and the node's world scale,
// placed where the node is now. One body per node; a second call replaces the first.
bool bodyNew(int32_t node, BodyTypeE type, ShapeTypeE shape, float a, float b, float c) {
BodyRecordT *record;
JPH::RefConst<JPH::Shape> joltShape;
Vec3T position;
QuatT rotation;
Vec3T scale;
float radius;
float height;
int32_t x;
if ((_world == nullptr) || !nodeValid(node)) {
return false;
}
bodyDelete(node);
sceneUpdateTransforms();
nodeGetWorldTransform(node, &position, &rotation, &scale);
switch (shape) {
case SHAPE_BOX:
joltShape = new JPH::BoxShape(JPH::Vec3(SDL_max(a * scale.x, MIN_DIMENSION) / 2.0f, SDL_max(b * scale.y, MIN_DIMENSION) / 2.0f, SDL_max(c * scale.z, MIN_DIMENSION) / 2.0f), 0.0f);
break;
case SHAPE_SPHERE:
joltShape = new JPH::SphereShape(SDL_max(a * SDL_max(scale.x, SDL_max(scale.y, scale.z)), MIN_DIMENSION));
break;
case SHAPE_CAPSULE:
radius = SDL_max(a * SDL_max(scale.x, scale.z), MIN_DIMENSION);
height = SDL_max(b * scale.y, MIN_DIMENSION);
joltShape = new JPH::CapsuleShape(SDL_max(height / 2.0f - radius, MIN_DIMENSION), radius);
break;
case SHAPE_CYLINDER:
radius = SDL_max(a * SDL_max(scale.x, scale.z), MIN_DIMENSION);
height = SDL_max(b * scale.y, MIN_DIMENSION);
joltShape = new JPH::CylinderShape(height / 2.0f, radius);
break;
default:
utilTrace("Physics: shape %d is not available yet.", (int32_t)shape);
return false;
}
for (x = 0; x < _world->bodyCount; x++) {
if (!_world->bodies[x].used) {
break;
}
}
if (x == _world->bodyCount) {
_world->bodies = (BodyRecordT *)SDL_realloc(_world->bodies, sizeof(BodyRecordT) * (size_t)(_world->bodyCount + 1));
if (_world->bodies == nullptr) {
utilDie("Out of memory allocating a physics body.");
}
_world->bodyCount++;
}
record = &_world->bodies[x];
memset(record, 0, sizeof(*record));
{
JPH::EMotionType motion = (type == BODY_STATIC) ? JPH::EMotionType::Static : ((type == BODY_KINEMATIC) ? JPH::EMotionType::Kinematic : JPH::EMotionType::Dynamic);
JPH::ObjectLayer layer = (type == BODY_STATIC) ? LAYER_NON_MOVING : LAYER_MOVING;
JPH::BodyCreationSettings settings(joltShape, JPH::RVec3(position.x, position.y, position.z), _fromQuat(rotation), motion, layer);
settings.mFriction = DEFAULT_FRICTION;
settings.mRestitution = DEFAULT_BOUNCE;
settings.mUserData = (JPH::uint64)(uint32_t)node;
record->id = _world->system->GetBodyInterface().CreateAndAddBody(settings, JPH::EActivation::Activate);
}
if (record->id.IsInvalid()) {
utilTrace("Physics: unable to create a body (too many?).");
return false;
}
record->node = node;
record->generation = nodeGetGeneration(node);
record->type = type;
record->enabled = true;
record->used = true;
return true;
}
bool bodySetAngularVelocity(int32_t node, Vec3T velocity) {
BodyRecordT *record = _find(node);
if ((record == nullptr) || (record->type == BODY_STATIC)) {
return false;
}
_world->system->GetBodyInterface().SetAngularVelocity(record->id, _fromVec3(velocity));
return true;
}
// 0 stops dead, 1 bounces back with everything it arrived with.
bool bodySetBounce(int32_t node, float bounce) {
BodyRecordT *record = _find(node);
if (record == nullptr) {
return false;
}
_world->system->GetBodyInterface().SetRestitution(record->id, SDL_clamp(bounce, 0.0f, 1.0f));
return true;
}
// Takes the body out of the world (it stops colliding and moving) and puts it back.
bool bodySetEnabled(int32_t node, bool enabled) {
BodyRecordT *record = _find(node);
if (record == nullptr) {
return false;
}
if (enabled && !record->enabled) {
_world->system->GetBodyInterface().AddBody(record->id, JPH::EActivation::Activate);
} else if (!enabled && record->enabled) {
_world->system->GetBodyInterface().RemoveBody(record->id);
}
record->enabled = enabled;
return true;
}
bool bodySetFriction(int32_t node, float friction) {
BodyRecordT *record = _find(node);
if (record == nullptr) {
return false;
}
_world->system->GetBodyInterface().SetFriction(record->id, SDL_max(friction, 0.0f));
return true;
}
// Rescales the body's mass and inertia (dynamic bodies only).
bool bodySetMass(int32_t node, float kilograms) {
BodyRecordT *record = _find(node);
if ((record == nullptr) || (record->type != BODY_DYNAMIC) || (kilograms <= 0.0f)) {
return false;
}
{
JPH::BodyLockWrite lock(_world->system->GetBodyLockInterface(), record->id);
if (!lock.Succeeded()) {
return false;
}
lock.GetBody().GetMotionProperties()->ScaleToMass(kilograms);
}
return true;
}
bool bodySetVelocity(int32_t node, Vec3T velocity) {
BodyRecordT *record = _find(node);
if ((record == nullptr) || (record->type == BODY_STATIC)) {
return false;
}
_world->system->GetBodyInterface().SetLinearVelocity(record->id, _fromVec3(velocity));
return true;
}
// ===== World =====
bool physicsAvailable(void) {
return _world != nullptr;
}
// Brings Jolt up: allocators, the type factory, a job pool sized to the machine, and an empty
// world. Refuses (returning false, 3D physics unavailable) on an x86 without SSE4.1 and 4.2, the
// level the library was compiled for, rather than faulting on the first instruction.
bool physicsInit(void) {
int32_t threads;
if (_world != nullptr) {
return true;
}
#if defined(JPH_USE_SSE4_2)
if (!SDL_HasSSE41() || !SDL_HasSSE42()) {
utilTrace("Physics: this CPU lacks SSE4.1/4.2; physics is unavailable.");
return false;
}
#endif
JPH::RegisterDefaultAllocator();
JPH::Trace = _trace;
JPH::Factory::sInstance = new JPH::Factory();
JPH::RegisterTypes();
_world = new WorldT();
_world->tempAllocator = new JPH::TempAllocatorImpl(TEMP_ALLOCATOR_BYTES);
threads = SDL_max(SDL_GetNumLogicalCPUCores() - 1, MIN_JOB_THREADS);
_world->jobs = new JPH::JobSystemThreadPool(JPH::cMaxPhysicsJobs, JPH::cMaxPhysicsBarriers, threads);
_world->system = new JPH::PhysicsSystem();
_world->system->Init(MAX_BODIES, 0, MAX_BODY_PAIRS, MAX_CONTACTS, _world->broadPhaseLayers, _world->objectVsBroadPhase, _world->objectPairs);
_world->enabled = true;
utilTrace("Physics: Jolt %d.%d.%d ready, %d job thread%s", JPH_VERSION_MAJOR, JPH_VERSION_MINOR, JPH_VERSION_PATCH, threads, (threads == 1) ? "" : "s");
return true;
}
void physicsQuit(void) {
int32_t x;
if (_world == nullptr) {
return;
}
for (x = 0; x < _world->bodyCount; x++) {
if (_world->bodies[x].used) {
_release(&_world->bodies[x]);
}
}
SDL_free(_world->bodies);
delete _world->system;
delete _world->jobs;
delete _world->tempAllocator;
delete _world;
_world = nullptr;
JPH::UnregisterTypes();
delete JPH::Factory::sInstance;
JPH::Factory::sInstance = nullptr;
}
// Pauses the simulation (bodies hold still) without losing it.
void physicsSetEnabled(bool enabled) {
if (_world != nullptr) {
_world->enabled = enabled;
}
}
void physicsSetGravity(Vec3T gravity) {
if (_world != nullptr) {
_world->system->SetGravity(_fromVec3(gravity));
}
}
// Once per frame, after animation: kinematic bodies are moved to their nodes, the world steps at
// a fixed rate for the time that has passed (at most a few steps, and none while the game is
// paused or physics is disabled), and dynamic bodies drive their nodes.
void physicsUpdate(bool advance) {
uint64_t now;
int32_t steps = 0;
int32_t x;
Vec3T position;
QuatT rotation;
Vec3T scale;
if (_world == nullptr) {
return;
}
now = SDL_GetTicksNS();
if (advance && _world->enabled && (_world->lastTick != 0)) {
_world->accumulator += (double)(now - _world->lastTick) / 1e9;
}
_world->lastTick = now;
if (_world->accumulator > STEP_SECONDS * MAX_STEPS_PER_FRAME) {
_world->accumulator = STEP_SECONDS * MAX_STEPS_PER_FRAME;
}
if (_world->accumulator < STEP_SECONDS) {
return;
}
sceneUpdateTransforms();
{
JPH::BodyInterface &bodies = _world->system->GetBodyInterface();
float dt = (float)(STEP_SECONDS * (int32_t)(_world->accumulator / STEP_SECONDS));
// Kinematic bodies go where their nodes went, with the velocity that implies.
for (x = 0; x < _world->bodyCount; x++) {
BodyRecordT *record = &_world->bodies[x];
if (!record->used || (record->type != BODY_KINEMATIC) || !record->enabled) {
continue;
}
if (!nodeValid(record->node) || (nodeGetGeneration(record->node) != record->generation)) {
_release(record);
continue;
}
nodeGetWorldTransform(record->node, &position, &rotation, &scale);
bodies.MoveKinematic(record->id, JPH::RVec3(position.x, position.y, position.z), _fromQuat(rotation), dt);
}
while ((_world->accumulator >= STEP_SECONDS) && (steps < MAX_STEPS_PER_FRAME)) {
_world->system->Update((float)STEP_SECONDS, 1, _world->tempAllocator, _world->jobs);
_world->accumulator -= STEP_SECONDS;
steps++;
}
// Dynamic bodies drive their nodes.
for (x = 0; x < _world->bodyCount; x++) {
BodyRecordT *record = &_world->bodies[x];
JPH::RVec3 where;
JPH::Quat how;
if (!record->used || (record->type != BODY_DYNAMIC) || !record->enabled) {
continue;
}
if (!nodeValid(record->node) || (nodeGetGeneration(record->node) != record->generation)) {
_release(record);
continue;
}
bodies.GetPositionAndRotation(record->id, where, how);
nodeSetWorldTransform(record->node, vec3((float)where.GetX(), (float)where.GetY(), (float)where.GetZ()), _toQuat(how));
}
}
}

View file

@ -1989,6 +1989,16 @@ Vec3T nodeGetWorldPosition(int32_t node) {
}
// The node's world-space position, rotation and scale as of the last transform update.
bool nodeGetWorldTransform(int32_t node, Vec3T *position, QuatT *rotation, Vec3T *scale) {
if (!nodeValid(node)) {
return false;
}
mat4Decompose(_scene.nodes[node].world, position, rotation, scale);
return true;
}
// Points the node's -Z at a world-space target (the camera and lights look down -Z). Only the
// node's own rotation changes, in its parent's space.
bool nodeLookAt(int32_t node, Vec3T target) {
@ -2167,6 +2177,36 @@ bool nodeSetVisible(int32_t node, bool visible) {
}
// Places the node at a world-space position and rotation by converting through its parent's
// world transform (what a physics body needs to drive a node under any parent). Scale is kept.
bool nodeSetWorldTransform(int32_t node, Vec3T position, QuatT rotation) {
NodeT *n;
Mat4T parentInverse;
Vec3T parentPosition;
QuatT parentRotation;
Vec3T parentScale;
QuatT parentInverseRotation;
if (!nodeValid(node)) {
return false;
}
n = &_scene.nodes[node];
if ((n->parent == NO_HANDLE) || (n->parent == SCENE_ROOT_NODE) || !mat4Invert(_scene.nodes[n->parent].world, &parentInverse)) {
n->translation = position;
n->rotation = quatNormalize(rotation);
return true;
}
mat4Decompose(_scene.nodes[n->parent].world, &parentPosition, &parentRotation, &parentScale);
parentInverseRotation = parentRotation;
parentInverseRotation.x = -parentInverseRotation.x;
parentInverseRotation.y = -parentInverseRotation.y;
parentInverseRotation.z = -parentInverseRotation.z;
n->translation = mat4TransformPoint(parentInverse, position);
n->rotation = quatNormalize(quatMultiply(parentInverseRotation, rotation));
return true;
}
bool nodeValid(int32_t node) {
return (node >= 0) && (node < _scene.nodeCount) && _scene.nodes[node].used;
}
@ -2644,6 +2684,17 @@ Vec3T sceneUnproject(float x, float y, float distance) {
}
// Rebuilds every node's world matrix now (sceneRender does it too); physics needs them before the
// step, after animation has moved the nodes.
void sceneUpdateTransforms(void) {
Mat4T identity = mat4Identity();
if (_scene.device != NULL) {
_updateWorld(SCENE_ROOT_NODE, &identity, true);
}
}
// Once per frame before sceneRender: copies every player a material shows into that feed's RGBA
// target with the 2D renderer (which converts YUV on the way), then flushes the renderer so the
// copies are queued ahead of the scene's own command buffer.

View file

@ -65,6 +65,7 @@ void sceneComputeNormals(SceneVertexT *vertices, int32_t vertexCount, co
void sceneSetBackground(uint8_t r, uint8_t g, uint8_t b, uint8_t a);
void sceneSetShadowSize(int32_t size);
Vec3T sceneUnproject(float x, float y, float distance);
void sceneUpdateTransforms(void);
void sceneUpdateVideo(SceneVideoSourceFn source);
bool cameraSet(int32_t node);
@ -114,6 +115,7 @@ Vec3T nodeGetPosition(int32_t node);
QuatT nodeGetRotation(int32_t node);
Vec3T nodeGetScale(int32_t node);
Vec3T nodeGetWorldPosition(int32_t node);
bool nodeGetWorldTransform(int32_t node, Vec3T *position, QuatT *rotation, Vec3T *scale);
bool nodeLookAt(int32_t node, Vec3T target);
bool nodeMove(int32_t node, Vec3T delta);
int32_t nodeNew(int32_t parent);
@ -126,6 +128,7 @@ bool nodeSetRotation(int32_t node, QuatT rotation);
bool nodeSetScale(int32_t node, Vec3T scale);
bool nodeSetSkin(int32_t node, const int32_t *joints, const Mat4T *inverseBind, int32_t count);
bool nodeSetVisible(int32_t node, bool visible);
bool nodeSetWorldTransform(int32_t node, Vec3T position, QuatT rotation);
bool nodeValid(int32_t node);

View file

@ -60,6 +60,7 @@ LSEC_API int luaopen_ssl_config(lua_State *L);
#include "vfs.h"
#include "scene.h"
#include "model.h"
#include "physics.h"
#include "videoPlayer.h"
#include "singe.h"
@ -437,6 +438,7 @@ static QuatT _argEuler(lua_State *L, const char *method, int32_t index);
static float *_argFloatTable(lua_State *L, const char *method, int32_t index, int32_t *count);
static FontT *_argFont(lua_State *L, const char *method, int32_t index);
static int32_t _argInteger(lua_State *L, const char *method, int32_t index);
static int32_t _argBody(lua_State *L, const char *method, int32_t index);
static int32_t _argMaterial(lua_State *L, const char *method, int32_t index);
static int32_t _argMesh(lua_State *L, const char *method, int32_t index);
static int32_t _argNode(lua_State *L, const char *method, int32_t index);
@ -526,6 +528,19 @@ static int32_t apiAnimationPlay(lua_State *L);
static int32_t apiAnimationResume(lua_State *L);
static int32_t apiAnimationSetTime(lua_State *L);
static int32_t apiAnimationStop(lua_State *L);
static int32_t apiBodyApplyForce(lua_State *L);
static int32_t apiBodyApplyImpulse(lua_State *L);
static int32_t apiBodyDelete(lua_State *L);
static int32_t apiBodyGetAngularVelocity(lua_State *L);
static int32_t apiBodyGetVelocity(lua_State *L);
static int32_t apiBodyIsResting(lua_State *L);
static int32_t apiBodyNew(lua_State *L);
static int32_t apiBodySetAngularVelocity(lua_State *L);
static int32_t apiBodySetBounce(lua_State *L);
static int32_t apiBodySetEnabled(lua_State *L);
static int32_t apiBodySetFriction(lua_State *L);
static int32_t apiBodySetMass(lua_State *L);
static int32_t apiBodySetVelocity(lua_State *L);
static int32_t apiCameraSet(lua_State *L);
static int32_t apiCameraSetOrthographic(lua_State *L);
static int32_t apiCameraSetPerspective(lua_State *L);
@ -635,6 +650,8 @@ static int32_t apiOverlayLine(lua_State *L);
static int32_t apiOverlayPlot(lua_State *L);
static int32_t apiOverlayPrint(lua_State *L);
static int32_t apiOverlaySetResolution(lua_State *L);
static int32_t apiPhysicsSetEnabled(lua_State *L);
static int32_t apiPhysicsSetGravity(lua_State *L);
static int32_t apiSceneEnable(lua_State *L);
static int32_t apiSceneGetSize(lua_State *L);
static int32_t apiSceneProject(lua_State *L);
@ -801,6 +818,17 @@ static int32_t _argInteger(lua_State *L, const char *method, int32_t index) {
}
// A node that carries a physics body, checked.
static int32_t _argBody(lua_State *L, const char *method, int32_t index) {
int32_t node = _argNode(L, method, index);
if (!bodyExists(node)) {
_luaDie(L, method, "Node %d has no body.", node);
}
return node;
}
// A material handle argument, checked.
static int32_t _argMaterial(lua_State *L, const char *method, int32_t index) {
int32_t material = _argInteger(L, method, index);
@ -2153,6 +2181,25 @@ static void _pushConstants(lua_State *L) {
lua_setglobal(L, "LIGHT_POINT");
lua_pushinteger(L, LIGHT_SPOT);
lua_setglobal(L, "LIGHT_SPOT");
// Physics bodies and shapes
lua_pushinteger(L, BODY_STATIC);
lua_setglobal(L, "BODY_STATIC");
lua_pushinteger(L, BODY_DYNAMIC);
lua_setglobal(L, "BODY_DYNAMIC");
lua_pushinteger(L, BODY_KINEMATIC);
lua_setglobal(L, "BODY_KINEMATIC");
lua_pushinteger(L, SHAPE_BOX);
lua_setglobal(L, "SHAPE_BOX");
lua_pushinteger(L, SHAPE_SPHERE);
lua_setglobal(L, "SHAPE_SPHERE");
lua_pushinteger(L, SHAPE_CAPSULE);
lua_setglobal(L, "SHAPE_CAPSULE");
lua_pushinteger(L, SHAPE_CYLINDER);
lua_setglobal(L, "SHAPE_CYLINDER");
lua_pushinteger(L, SHAPE_HULL);
lua_setglobal(L, "SHAPE_HULL");
lua_pushinteger(L, SHAPE_MESH);
lua_setglobal(L, "SHAPE_MESH");
lua_pushinteger(L, -1);
lua_setglobal(L, "SOUND_ERROR_INVALID");
@ -2704,6 +2751,161 @@ static int32_t apiAnimationStop(lua_State *L) {
}
// bodyApplyForce(node, fx, fy, fz [, px, py, pz]): this step, at the centre or a world point
static int32_t apiBodyApplyForce(lua_State *L) {
int32_t node;
Vec3T force;
Vec3T at;
_argCheck(L, "bodyApplyForce", 4, 7);
node = _argBody(L, "bodyApplyForce", 1);
force = _argVec3(L, "bodyApplyForce", 2);
if (lua_gettop(L) >= 7) {
at = _argVec3(L, "bodyApplyForce", 5);
bodyApplyForce(node, force, &at);
} else {
bodyApplyForce(node, force, NULL);
}
return 0;
}
// bodyApplyImpulse(node, ix, iy, iz [, px, py, pz]): an instant change of momentum
static int32_t apiBodyApplyImpulse(lua_State *L) {
int32_t node;
Vec3T impulse;
Vec3T at;
_argCheck(L, "bodyApplyImpulse", 4, 7);
node = _argBody(L, "bodyApplyImpulse", 1);
impulse = _argVec3(L, "bodyApplyImpulse", 2);
if (lua_gettop(L) >= 7) {
at = _argVec3(L, "bodyApplyImpulse", 5);
bodyApplyImpulse(node, impulse, &at);
} else {
bodyApplyImpulse(node, impulse, NULL);
}
return 0;
}
// bodyDelete(node)
static int32_t apiBodyDelete(lua_State *L) {
_argCheck(L, "bodyDelete", 1, 1);
bodyDelete(_argBody(L, "bodyDelete", 1));
return 0;
}
// x, y, z = bodyGetAngularVelocity(node): radians per second about each axis
static int32_t apiBodyGetAngularVelocity(lua_State *L) {
_argCheck(L, "bodyGetAngularVelocity", 1, 1);
return _pushVec3(L, bodyGetAngularVelocity(_argBody(L, "bodyGetAngularVelocity", 1)));
}
// x, y, z = bodyGetVelocity(node): units per second
static int32_t apiBodyGetVelocity(lua_State *L) {
_argCheck(L, "bodyGetVelocity", 1, 1);
return _pushVec3(L, bodyGetVelocity(_argBody(L, "bodyGetVelocity", 1)));
}
// resting = bodyIsResting(node): asleep
static int32_t apiBodyIsResting(lua_State *L) {
_argCheck(L, "bodyIsResting", 1, 1);
lua_pushboolean(L, bodyIsResting(_argBody(L, "bodyIsResting", 1)));
return 1;
}
// bodyNew(node, type, shape, a [, b [, c]]): BODY_* and SHAPE_*; a, b, c size the shape
static int32_t apiBodyNew(lua_State *L) {
int32_t node;
int32_t type;
int32_t shape;
float dims[3] = { 0.0f, 0.0f, 0.0f };
int32_t x;
_argCheck(L, "bodyNew", 3, 6);
node = _argNode(L, "bodyNew", 1);
type = _argInteger(L, "bodyNew", 2);
shape = _argInteger(L, "bodyNew", 3);
if ((type < BODY_STATIC) || (type > BODY_KINEMATIC)) {
_luaDie(L, "bodyNew", "Unknown body type %d.", type);
}
if ((shape < SHAPE_BOX) || (shape > SHAPE_MESH)) {
_luaDie(L, "bodyNew", "Unknown shape %d.", shape);
}
for (x = 0; x < 3; x++) {
if (lua_gettop(L) >= 4 + x) {
dims[x] = (float)_argNumber(L, "bodyNew", 4 + x);
}
}
if (!physicsAvailable()) {
_luaDie(L, "bodyNew", "Physics is not available on this machine.");
}
if (!bodyNew(node, (BodyTypeE)type, (ShapeTypeE)shape, dims[0], dims[1], dims[2])) {
_luaDie(L, "bodyNew", "Unable to create the body.");
}
_luaTrace(L, "bodyNew", "node %d type %d shape %d", node, type, shape);
return 0;
}
// bodySetAngularVelocity(node, x, y, z)
static int32_t apiBodySetAngularVelocity(lua_State *L) {
_argCheck(L, "bodySetAngularVelocity", 4, 4);
bodySetAngularVelocity(_argBody(L, "bodySetAngularVelocity", 1), _argVec3(L, "bodySetAngularVelocity", 2));
return 0;
}
// bodySetBounce(node, 0..1)
static int32_t apiBodySetBounce(lua_State *L) {
_argCheck(L, "bodySetBounce", 2, 2);
bodySetBounce(_argBody(L, "bodySetBounce", 1), (float)_argNumber(L, "bodySetBounce", 2));
return 0;
}
// bodySetEnabled(node, bool): out of the world and back
static int32_t apiBodySetEnabled(lua_State *L) {
_argCheck(L, "bodySetEnabled", 2, 2);
bodySetEnabled(_argBody(L, "bodySetEnabled", 1), _argBoolean(L, "bodySetEnabled", 2));
return 0;
}
// bodySetFriction(node, friction)
static int32_t apiBodySetFriction(lua_State *L) {
_argCheck(L, "bodySetFriction", 2, 2);
bodySetFriction(_argBody(L, "bodySetFriction", 1), (float)_argNumber(L, "bodySetFriction", 2));
return 0;
}
// bodySetMass(node, kilograms): dynamic bodies
static int32_t apiBodySetMass(lua_State *L) {
int32_t node;
_argCheck(L, "bodySetMass", 2, 2);
node = _argBody(L, "bodySetMass", 1);
if (!bodySetMass(node, (float)_argNumber(L, "bodySetMass", 2))) {
_luaDie(L, "bodySetMass", "Node %d is not a dynamic body, or the mass is not positive.", node);
}
return 0;
}
// bodySetVelocity(node, x, y, z)
static int32_t apiBodySetVelocity(lua_State *L) {
_argCheck(L, "bodySetVelocity", 4, 4);
bodySetVelocity(_argBody(L, "bodySetVelocity", 1), _argVec3(L, "bodySetVelocity", 2));
return 0;
}
// Any node can be the camera (it looks down its own -Z); -1 restores the default view.
static int32_t apiCameraSet(lua_State *L) {
int32_t node;
@ -4426,6 +4628,22 @@ static int32_t apiOverlaySetResolution(lua_State *L) {
}
// physicsSetEnabled(bool): pauses the simulation without losing it
static int32_t apiPhysicsSetEnabled(lua_State *L) {
_argCheck(L, "physicsSetEnabled", 1, 1);
physicsSetEnabled(_argBoolean(L, "physicsSetEnabled", 1));
return 0;
}
// physicsSetGravity(x, y, z): default 0, -9.81, 0
static int32_t apiPhysicsSetGravity(lua_State *L) {
_argCheck(L, "physicsSetGravity", 3, 3);
physicsSetGravity(_argVec3(L, "physicsSetGravity", 1));
return 0;
}
// scriptExecute(config) Runs another script after this one ends.
// Turns the 3D layer on or off.
static int32_t apiSceneEnable(lua_State *L) {
@ -5852,6 +6070,7 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
_global.window = window;
_global.renderer = renderer;
sceneInit(device, renderer);
physicsInit();
// Local copy of config
_global.conf = cloneConf(conf);
@ -5942,6 +6161,19 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
lua_register(_global.luaContext, "animationResume", apiAnimationResume); // 3.00
lua_register(_global.luaContext, "animationSetTime", apiAnimationSetTime); // 3.00
lua_register(_global.luaContext, "animationStop", apiAnimationStop); // 3.00
lua_register(_global.luaContext, "bodyApplyForce", apiBodyApplyForce); // 3.00
lua_register(_global.luaContext, "bodyApplyImpulse", apiBodyApplyImpulse); // 3.00
lua_register(_global.luaContext, "bodyDelete", apiBodyDelete); // 3.00
lua_register(_global.luaContext, "bodyGetAngularVelocity", apiBodyGetAngularVelocity); // 3.00
lua_register(_global.luaContext, "bodyGetVelocity", apiBodyGetVelocity); // 3.00
lua_register(_global.luaContext, "bodyIsResting", apiBodyIsResting); // 3.00
lua_register(_global.luaContext, "bodyNew", apiBodyNew); // 3.00
lua_register(_global.luaContext, "bodySetAngularVelocity", apiBodySetAngularVelocity); // 3.00
lua_register(_global.luaContext, "bodySetBounce", apiBodySetBounce); // 3.00
lua_register(_global.luaContext, "bodySetEnabled", apiBodySetEnabled); // 3.00
lua_register(_global.luaContext, "bodySetFriction", apiBodySetFriction); // 3.00
lua_register(_global.luaContext, "bodySetMass", apiBodySetMass); // 3.00
lua_register(_global.luaContext, "bodySetVelocity", apiBodySetVelocity); // 3.00
lua_register(_global.luaContext, "colorBackground", apiColorBackground); // 1.xx
lua_register(_global.luaContext, "colorForeground", apiColorForeground); // 1.xx
@ -6059,6 +6291,8 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
lua_register(_global.luaContext, "overlayPrint", apiOverlayPrint); // 1.xx
lua_register(_global.luaContext, "overlaySetResolution", apiOverlaySetResolution); // 2.00
lua_register(_global.luaContext, "physicsSetEnabled", apiPhysicsSetEnabled); // 3.00
lua_register(_global.luaContext, "physicsSetGravity", apiPhysicsSetGravity); // 3.00
lua_register(_global.luaContext, "sceneEnable", apiSceneEnable); // 3.00
lua_register(_global.luaContext, "sceneGetSize", apiSceneGetSize); // 3.00
lua_register(_global.luaContext, "sceneProject", apiSceneProject); // 3.00
@ -6614,6 +6848,7 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
}
// 3D scene
modelUpdate(!_global.frozen);
physicsUpdate(!_global.frozen);
sceneUpdateVideo(_sceneVideoSource);
sceneTexture = sceneRender();
if (sceneTexture != NULL) {
@ -6662,6 +6897,7 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
_progTrace("Destroying overlay");
SDL_DestroyTexture(_global.pauseTexture);
modelQuit();
physicsQuit();
sceneQuit();
SDL_DestroyTexture(_global.overlayTexture);
SDL_DestroySurface(_global.overlay);

BIN
testScripts/Box.png (Stored with Git LFS) Executable file

Binary file not shown.

BIN
testScripts/Models/BoxAnimated.glb (Stored with Git LFS) Normal file

Binary file not shown.

BIN
testScripts/Models/Duck.glb (Stored with Git LFS) Normal file

Binary file not shown.

BIN
testScripts/Models/Fox.glb (Stored with Git LFS) Normal file

Binary file not shown.

26
testScripts/README.md Normal file
View file

@ -0,0 +1,26 @@
# Test scripts
The Lua scripts used to verify the 3D scene and physics work, one per stage
of PLAN.md sections 18 and 19, with everything they load: the Khronos
Duck, Fox and BoxAnimated sample models under Models/, and Box.png. The
disc and the font are the engine's own, addressed as
Singe/menuBackground.mkv and Singe/FreeSansBold.ttf like any engine
asset. This directory is a complete Singe game directory. Run one from the singe directory with
.builddir/Singe-v3.00-Linux-x86_64 -w -d data -v Singe/menuBackground.mkv testScripts/scene6.singe
or pack it (singe --pack testScripts testScripts.game) and run an entry
with --entry N (the numbers follow games.dat). Every script takes a
screenshot or two and quits by itself; the results are in screenshots/.
| Script | Entry | Stage | What it shows |
|-----------------|-------|-------|------------------------------------------------------------|
| scenetest.singe | 1 | 18.1 | The 3D layer: a translucent colour over the disc, text on top |
| scene2.singe | 2 | 18.2 | Primitives, materials, lights, camera, sceneProject label |
| scene3.singe | 3 | 18.3 | glTF models: two Ducks, BoxAnimated, loose and packed |
| scene4.singe | 4 | 18.4 | Node animation at three speeds, one paused |
| scene5.singe | 5 | 18.5 | Skinning: three Foxes in Survey, Walk and Run |
| scene6.singe | 6 | 18.6 | The disc and a loaded video as materials, sceneUnproject |
| scene7.singe | 7 | 18.6b | Shadows: sun, sun and spot together, a point light inside |
| scene8.singe | 8 | 18 | An arcade cabinet from primitives with the disc on screen |
| scene9.singe | 9 | 19.2 | Physics: a crate stack, a ball, a kinematic paddle |

101
testScripts/games.dat Normal file
View file

@ -0,0 +1,101 @@
GAMES = {
{
TITLE = "Scene1",
SCRIPT = "testScripts/scenetest.singe",
VIDEO = "Singe/menuBackground.mkv",
DESCRIPTION = "Stage 1 of the 3D scene: a translucent layer over the disc.",
YEAR = 2026,
GENRE = "Test",
PLATFORM = "Singe",
DEVELOPER = "Test",
PUBLISHER = "Test",
},
{
TITLE = "Scene2",
SCRIPT = "testScripts/scene2.singe",
VIDEO = "Singe/menuBackground.mkv",
DESCRIPTION = "Stage 2: primitives, materials, lights.",
YEAR = 2026,
GENRE = "Test",
PLATFORM = "Singe",
DEVELOPER = "Test",
PUBLISHER = "Test",
},
{
TITLE = "Scene3",
SCRIPT = "testScripts/scene3.singe",
VIDEO = "Singe/menuBackground.mkv",
DESCRIPTION = "Stage 3: glTF models.",
YEAR = 2026,
GENRE = "Test",
PLATFORM = "Singe",
DEVELOPER = "Test",
PUBLISHER = "Test",
},
{
TITLE = "Scene4",
SCRIPT = "testScripts/scene4.singe",
VIDEO = "Singe/menuBackground.mkv",
DESCRIPTION = "Stage 4: node animation.",
YEAR = 2026,
GENRE = "Test",
PLATFORM = "Singe",
DEVELOPER = "Test",
PUBLISHER = "Test",
},
{
TITLE = "Scene5",
SCRIPT = "testScripts/scene5.singe",
VIDEO = "Singe/menuBackground.mkv",
DESCRIPTION = "Stage 5: skinning.",
YEAR = 2026,
GENRE = "Test",
PLATFORM = "Singe",
DEVELOPER = "Test",
PUBLISHER = "Test",
},
{
TITLE = "Scene6",
SCRIPT = "testScripts/scene6.singe",
VIDEO = "Singe/menuBackground.mkv",
DESCRIPTION = "Stage 6: video materials, unproject, antialiasing.",
YEAR = 2026,
GENRE = "Test",
PLATFORM = "Singe",
DEVELOPER = "Test",
PUBLISHER = "Test",
},
{
TITLE = "Scene7",
SCRIPT = "testScripts/scene7.singe",
VIDEO = "Singe/menuBackground.mkv",
DESCRIPTION = "Stage 6b: shadows.",
YEAR = 2026,
GENRE = "Test",
PLATFORM = "Singe",
DEVELOPER = "Test",
PUBLISHER = "Test",
},
{
TITLE = "Cabinet",
SCRIPT = "testScripts/scene8.singe",
VIDEO = "Singe/menuBackground.mkv",
DESCRIPTION = "An arcade cabinet built from primitives, the disc on its screen.",
YEAR = 2026,
GENRE = "Test",
PLATFORM = "Singe",
DEVELOPER = "Test",
PUBLISHER = "Test",
},
{
TITLE = "Physics",
SCRIPT = "testScripts/scene9.singe",
VIDEO = "Singe/menuBackground.mkv",
DESCRIPTION = "A crate stack, a ball and a kinematic paddle on Jolt Physics.",
YEAR = 2026,
GENRE = "Test",
PLATFORM = "Singe",
DEVELOPER = "Test",
PUBLISHER = "Test",
},
}

82
testScripts/scene2.singe Normal file
View file

@ -0,0 +1,82 @@
-- Stage 2 test: a lit spinning cube, a textured plane and a glass sphere over the disc, with
-- overlay text pinned to the cube through sceneProject.
local font = fontLoad("Singe/FreeSansBold.ttf", 28)
local boxArt = spriteLoad("testScripts/Box.png")
local frames = 0
fontSelect(font)
discPlay()
sceneEnable(true)
sceneSetBackground(0, 0, 0, 0)
sceneSetAmbient(40, 40, 60)
local red = materialNew()
materialSetColor(red, 220, 40, 40)
materialSetRoughness(red, 0.3)
materialSetMetallic(red, 0.2)
local tiled = materialNew()
materialSetTexture(tiled, boxArt)
local glass = materialNew()
materialSetColor(glass, 80, 160, 255, 110)
materialSetBlend(glass, true)
materialSetRoughness(glass, 0.1)
local gold = materialNew()
materialSetColor(gold, 255, 200, 60)
materialSetMetallic(gold, 1.0)
materialSetRoughness(gold, 0.35)
local cube = nodeNew()
nodeSetMesh(cube, meshBox(1.2, 1.2, 1.2), red)
nodeSetPosition(cube, -1.6, 0.3, 0)
local floor = nodeNew()
nodeSetMesh(floor, meshPlane(6, 4), tiled)
nodeSetPosition(floor, 0, -1, -1)
local ball = nodeNew()
nodeSetMesh(ball, meshSphere(0.7, 32), glass)
nodeSetPosition(ball, 1.4, 0.2, 0.5)
local ring = nodeNew()
nodeSetMesh(ring, meshTorus(0.8, 0.2, 40), gold)
nodeSetPosition(ring, 1.4, 0.2, 0.5)
local cone = nodeNew()
nodeSetMesh(cone, meshCone(0.4, 0.9, 24), gold)
nodeSetPosition(cone, 0, -0.55, 1.2)
local sun = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(sun, 2, 4, 3)
nodeLookAt(sun, 0, 0, 0)
lightSetIntensity(sun, 1.2)
local lamp = lightNew(LIGHT_POINT)
nodeSetPosition(lamp, -2, 1.5, 2)
lightSetColor(lamp, 120, 200, 255)
lightSetIntensity(lamp, 6)
lightSetRange(lamp, 8)
local camera = nodeNew()
nodeSetPosition(camera, 0, 1.6, 5.5)
nodeLookAt(camera, 0, 0, 0)
cameraSet(camera)
cameraSetPerspective(55, 0.1, 100)
function onOverlayUpdate()
frames = frames + 1
nodeRotate(cube, 0.8, 1.7, 0)
nodeRotate(ring, 0, 2.0, 1.0)
overlayClear()
local x, y, depth, inFront = sceneProject(nodeGetWorldPosition(cube))
fontPrint(x - 40, y - 70, "cube")
fontPrint(20, 20, "Stage 2, frame " .. frames)
if frames == 90 then
singeScreenshot()
end
if frames == 120 then
singeQuit()
end
end

53
testScripts/scene3.singe Normal file
View file

@ -0,0 +1,53 @@
-- Stage 3 test: glTF models. The Khronos Duck (textured) twice at different scales, and
-- BoxAnimated (two meshes, one nested), over the disc.
local font = fontLoad("Singe/FreeSansBold.ttf", 28)
local frames = 0
fontSelect(font)
discPlay()
sceneEnable(true)
sceneSetBackground(0, 0, 0, 0)
sceneSetAmbient(60, 60, 70)
local duckModel = modelLoad("testScripts/Models/Duck.glb")
local boxModel = modelLoad("testScripts/Models/BoxAnimated.glb")
local duck = modelInstance(duckModel)
nodeSetPosition(duck, -1.5, -1, 0)
nodeSetScale(duck, 1.0)
local duck2 = modelInstance(duckModel)
nodeSetPosition(duck2, 1.8, -1, -0.5)
nodeSetScale(duck2, 0.6)
nodeSetRotation(duck2, 0, -60, 0)
local boxes = modelInstance(boxModel)
nodeSetPosition(boxes, 0.2, -1, 1.2)
nodeSetScale(boxes, 0.4)
local sun = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(sun, 2, 4, 3)
nodeLookAt(sun, 0, 0, 0)
lightSetIntensity(sun, 1.5)
local camera = nodeNew()
nodeSetPosition(camera, 0, 1.2, 5)
nodeLookAt(camera, 0, 0, 0)
cameraSet(camera)
debugPrint("duck children " .. #nodeGetChildren(duck) .. " boxes children " .. #nodeGetChildren(boxes) .. " box anims " .. #modelGetAnimations(boxModel))
local inner = nodeFind("inner_box", boxes)
debugPrint("inner_box " .. tostring(inner))
function onOverlayUpdate()
frames = frames + 1
nodeRotate(duck, 0, 1.5, 0)
overlayClear()
fontPrint(20, 20, "Stage 3, frame " .. frames)
if frames == 60 then
singeScreenshot()
end
if frames == 80 then
singeQuit()
end
end

53
testScripts/scene4.singe Normal file
View file

@ -0,0 +1,53 @@
-- Stage 4 test: node animation. BoxAnimated's lid rotates and the inner box rises; two
-- instances at different speeds, a third paused at a fixed time.
local font = fontLoad("Singe/FreeSansBold.ttf", 28)
local frames = 0
fontSelect(font)
discPlay()
sceneEnable(true)
sceneSetBackground(0, 0, 0, 0)
sceneSetAmbient(60, 60, 70)
local boxModel = modelLoad("testScripts/Models/BoxAnimated.glb")
local names = modelGetAnimations(boxModel)
debugPrint("animations " .. #names .. " first '" .. tostring(names[1]) .. "'")
local a = modelInstance(boxModel)
nodeSetPosition(a, -2.2, -1, 0)
nodeSetScale(a, 0.5)
animationPlay(a, 1, true)
local b = modelInstance(boxModel)
nodeSetPosition(b, 0.2, -1, 0)
nodeSetScale(b, 0.5)
animationPlay(b, 1, true, 3.0)
local c = modelInstance(boxModel)
nodeSetPosition(c, 2.6, -1, 0)
nodeSetScale(c, 0.5)
animationPlay(c, 1, false)
animationSetTime(c, 1.5)
animationPause(c)
local sun = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(sun, 2, 4, 3)
nodeLookAt(sun, 0, 0, 0)
lightSetIntensity(sun, 1.5)
local camera = nodeNew()
nodeSetPosition(camera, 0, 1.5, 6)
nodeLookAt(camera, 0, 0, 0)
cameraSet(camera)
function onOverlayUpdate()
frames = frames + 1
overlayClear()
fontPrint(20, 20, string.format("Stage 4, frame %d a=%.2f b=%.2f c=%.2f playing=%s", frames, animationGetTime(a), animationGetTime(b), animationGetTime(c), tostring(animationIsPlaying(c))))
if frames == 30 or frames == 75 then
singeScreenshot()
end
if frames == 90 then
singeQuit()
end
end

45
testScripts/scene5.singe Normal file
View file

@ -0,0 +1,45 @@
-- Stage 5 test: skinning. Three Foxes: Survey, Walk and Run.
local font = fontLoad("Singe/FreeSansBold.ttf", 28)
local frames = 0
fontSelect(font)
discPlay()
sceneEnable(true)
sceneSetBackground(0, 0, 0, 0)
sceneSetAmbient(70, 70, 80)
local foxModel = modelLoad("testScripts/Models/Fox.glb")
local names = modelGetAnimations(foxModel)
debugPrint("fox animations: " .. table.concat(names, ", "))
local foxes = {}
for i, name in ipairs(names) do
local fox = modelInstance(foxModel)
nodeSetPosition(fox, (i - 2) * 2.4, -1, 0)
nodeSetScale(fox, 0.018)
nodeSetRotation(fox, 0, -30, 0)
animationPlay(fox, name, true)
foxes[i] = fox
end
local sun = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(sun, 2, 4, 3)
nodeLookAt(sun, 0, 0, 0)
lightSetIntensity(sun, 1.4)
local camera = nodeNew()
nodeSetPosition(camera, 0, 1.2, 6)
nodeLookAt(camera, 0, 0, 0)
cameraSet(camera)
function onOverlayUpdate()
frames = frames + 1
overlayClear()
fontPrint(20, 20, "Stage 5, frame " .. frames)
if frames == 40 or frames == 70 then
singeScreenshot()
end
if frames == 80 then
singeQuit()
end
end

75
testScripts/scene6.singe Normal file
View file

@ -0,0 +1,75 @@
-- Stage 6 test: the disc as a material on a tilted screen, a loaded video on a sphere, a small
-- marker placed with sceneUnproject at the overlay's centre, and antialiasing.
local font = fontLoad("Singe/FreeSansBold.ttf", 28)
local clip = videoLoad("Singe/menuBackground.mkv")
local frames = 0
fontSelect(font)
discPlay()
videoPlay(clip)
sceneEnable(true)
sceneSetBackground(20, 20, 40, 255)
sceneSetAmbient(80, 80, 90)
local screen = materialNew()
materialSetVideo(screen)
materialSetUnlit(screen, true)
local ballSkin = materialNew()
materialSetVideo(ballSkin, clip)
local cabinet = materialNew()
materialSetColor(cabinet, 60, 60, 70)
materialSetRoughness(cabinet, 0.6)
local marker = materialNew()
materialSetColor(marker, 255, 60, 60)
materialSetEmissive(marker, 120, 0, 0)
local body = nodeNew()
nodeSetMesh(body, meshBox(3.2, 2.4, 0.4), cabinet)
nodeSetPosition(body, -0.6, 0.2, -0.3)
nodeSetRotation(body, 0, 25, 0)
local tv = nodeNew(body)
nodeSetMesh(tv, meshPlane(3.0, 2.2), screen)
nodeSetPosition(tv, 0, 0, 0.21)
nodeSetRotation(tv, 90, 0, 0)
local ball = nodeNew()
nodeSetMesh(ball, meshSphere(0.7, 48), ballSkin)
nodeSetPosition(ball, 2.0, -0.2, 0.8)
local width, height = sceneGetSize()
local cx, cy, cz = sceneUnproject(width / 2, height / 2, 4.0)
local dot = nodeNew()
nodeSetMesh(dot, meshSphere(0.08, 16), marker)
local sun = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(sun, 2, 4, 3)
nodeLookAt(sun, 0, 0, 0)
lightSetIntensity(sun, 1.3)
local camera = nodeNew()
nodeSetPosition(camera, 0, 0.8, 5.5)
nodeLookAt(camera, 0, 0, 0)
cameraSet(camera)
function onOverlayUpdate()
frames = frames + 1
nodeRotate(ball, 0, 1.2, 0)
if frames == 2 then
-- After the first render the camera matrices exist; place the marker 4 units in at the centre.
cx, cy, cz = sceneUnproject(width / 2, height / 2, 4.0)
nodeSetPosition(dot, cx, cy, cz)
debugPrint(string.format("unproject centre -> %.2f %.2f %.2f", cx, cy, cz))
end
overlayClear()
fontPrint(20, 20, "Stage 6, frame " .. frames)
if frames == 80 then
singeScreenshot()
end
if frames == 100 then
singeQuit()
end
end

96
testScripts/scene7.singe Normal file
View file

@ -0,0 +1,96 @@
-- Stage 6b test: shadows. A floor, a cube, a duck and a walking fox under a directional sun
-- (first screenshot), then the same scene lit by a spot light overhead (second screenshot).
local font = fontLoad("Singe/FreeSansBold.ttf", 28)
local frames = 0
fontSelect(font)
discPlay()
sceneEnable(true)
sceneSetBackground(30, 30, 50, 255)
sceneSetAmbient(50, 50, 60)
local grey = materialNew()
materialSetColor(grey, 200, 200, 200)
materialSetRoughness(grey, 0.9)
local red = materialNew()
materialSetColor(red, 220, 50, 50)
local floor = nodeNew()
nodeSetMesh(floor, meshPlane(10, 10), grey)
nodeSetPosition(floor, 0, -1, 0)
local wall = nodeNew()
nodeSetMesh(wall, meshBox(10, 4, 0.2), grey)
nodeSetPosition(wall, 0, 1, -3)
local cube = nodeNew()
nodeSetMesh(cube, meshBox(1, 1, 1), red)
nodeSetPosition(cube, -2, -0.5, 0)
nodeSetRotation(cube, 0, 30, 0)
local duck = modelInstance(modelLoad("testScripts/Models/Duck.glb"))
nodeSetPosition(duck, 0.5, -1, 0.5)
nodeSetRotation(duck, 0, -40, 0)
local fox = modelInstance(modelLoad("testScripts/Models/Fox.glb"))
nodeSetPosition(fox, 2.5, -1, 0)
nodeSetScale(fox, 0.015)
nodeSetRotation(fox, 0, -110, 0)
animationPlay(fox, "Walk", true)
local sun = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(sun, 3, 6, 4)
nodeLookAt(sun, 0, 0, 0)
lightSetIntensity(sun, 1.4)
lightSetShadow(sun, true)
local lamp = lightNew(LIGHT_SPOT)
nodeSetPosition(lamp, 0, 5, 1.5)
nodeLookAt(lamp, 0, -1, 0)
lightSetIntensity(lamp, 40)
lightSetCone(lamp, 30, 42)
nodeSetVisible(lamp, false)
local bulb = lightNew(LIGHT_POINT)
nodeSetPosition(bulb, 0.8, 0.4, 1.2)
lightSetColor(bulb, 255, 220, 180)
lightSetIntensity(bulb, 12)
lightSetRange(bulb, 12)
nodeSetVisible(bulb, false)
local camera = nodeNew()
nodeSetPosition(camera, 0, 2.5, 7)
nodeLookAt(camera, 0, -0.3, 0)
cameraSet(camera)
function onOverlayUpdate()
frames = frames + 1
overlayClear()
fontPrint(20, 20, "Shadows, frame " .. frames)
if frames == 40 then
singeScreenshot()
end
if frames == 45 then
-- Sun and spot casting at once.
nodeSetVisible(lamp, true)
lightSetShadow(lamp, true)
end
if frames == 80 then
singeScreenshot()
end
if frames == 85 then
-- A bulb inside the scene: cube-map shadows in every direction.
nodeSetVisible(sun, false)
nodeSetVisible(lamp, false)
nodeSetVisible(bulb, true)
lightSetShadow(bulb, true)
sceneSetAmbient(25, 25, 30)
end
if frames == 120 then
singeScreenshot()
end
if frames == 130 then
singeQuit()
end
end

93
testScripts/scene8.singe Normal file
View file

@ -0,0 +1,93 @@
-- An arcade cabinet built from primitives, with the laserdisc playing on its screen.
local font = fontLoad("Singe/FreeSansBold.ttf", 28)
local frames = 0
fontSelect(font)
discPlay()
sceneEnable(true)
sceneSetBackground(18, 18, 30, 255)
sceneSetAmbient(70, 70, 85)
local black = materialNew()
materialSetColor(black, 25, 25, 28)
materialSetRoughness(black, 0.55)
local trim = materialNew()
materialSetColor(trim, 230, 60, 40)
materialSetRoughness(trim, 0.4)
local panelLook = materialNew()
materialSetColor(panelLook, 40, 40, 60)
materialSetRoughness(panelLook, 0.5)
local screen = materialNew()
materialSetVideo(screen)
materialSetUnlit(screen, true)
local marqueeLook = materialNew()
materialSetColor(marqueeLook, 255, 210, 80)
materialSetEmissive(marqueeLook, 120, 90, 20)
local floorLook = materialNew()
materialSetColor(floorLook, 120, 110, 100)
materialSetRoughness(floorLook, 0.9)
local floor = nodeNew()
nodeSetMesh(floor, meshPlane(12, 12), floorLook)
nodeSetPosition(floor, 0, -1.8, 0)
-- The cabinet: one parent node so the whole thing can be turned.
local cabinet = nodeNew()
nodeSetPosition(cabinet, 0, 0, 0)
nodeSetRotation(cabinet, 0, -28, 0)
local function part(parent, mesh, material, x, y, z, rx, ry, rz)
local n = nodeNew(parent)
nodeSetMesh(n, mesh, material)
nodeSetPosition(n, x, y, z)
nodeSetRotation(n, rx or 0, ry or 0, rz or 0)
return n
end
part(cabinet, meshBox(1.6, 1.4, 1.3), black, 0, -1.1, 0) -- base
part(cabinet, meshBox(1.6, 1.9, 1.0), black, 0, 0.55, -0.15) -- body behind the screen
part(cabinet, meshBox(1.6, 0.5, 1.3), black, 0, 1.75, 0) -- head
part(cabinet, meshBox(1.5, 0.36, 0.05), marqueeLook, 0, 1.75, 0.66) -- marquee
part(cabinet, meshBox(1.6, 0.12, 0.7), panelLook, 0, -0.45, 0.55, -12) -- control panel
part(cabinet, meshCylinder(0.05, 0.28, 16), trim, -0.35, -0.28, 0.55, -12) -- joystick
part(cabinet, meshSphere(0.09, 16), trim, -0.35, -0.13, 0.58) -- joystick ball
part(cabinet, meshCylinder(0.07, 0.04, 16), trim, 0.2, -0.36, 0.5, -12) -- button
part(cabinet, meshCylinder(0.07, 0.04, 16), trim, 0.42, -0.36, 0.5, -12) -- button
part(cabinet, meshBox(0.06, 1.9, 0.08), trim, -0.77, 0.55, 0.38) -- side trim
part(cabinet, meshBox(0.06, 1.9, 0.08), trim, 0.77, 0.55, 0.38)
part(cabinet, meshBox(1.42, 1.1, 0.06), black, 0, 0.6, 0.34, -8) -- bezel
part(cabinet, meshPlane(1.3, 0.98), screen, 0, 0.6, 0.38, 82) -- the screen, tilted back a little
local sun = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(sun, -3, 5, 4)
nodeLookAt(sun, 0, 0, 0)
lightSetIntensity(sun, 1.1)
lightSetShadow(sun, true)
local glow = lightNew(LIGHT_POINT)
nodeSetPosition(glow, 0.4, 0.9, 1.2)
lightSetColor(glow, 180, 200, 255)
lightSetIntensity(glow, 3)
lightSetRange(glow, 6)
local camera = nodeNew()
nodeSetPosition(camera, 1.6, 0.9, 4.6)
nodeLookAt(camera, 0, 0.2, 0)
cameraSet(camera)
cameraSetPerspective(45, 0.1, 100)
function onOverlayUpdate()
frames = frames + 1
overlayClear()
if frames == 22 then
singeScreenshot()
end
if frames == 30 then
singeQuit()
end
end

94
testScripts/scene9.singe Normal file
View file

@ -0,0 +1,94 @@
-- Physics stage 2: a stack of crates and a ball dropped onto the floor in front of the cabinet,
-- then a kinematic paddle sweeps through the stack.
local font = fontLoad("Singe/FreeSansBold.ttf", 28)
local frames = 0
fontSelect(font)
discPlay()
sceneEnable(true)
sceneSetBackground(18, 18, 30, 255)
sceneSetAmbient(70, 70, 85)
local floorLook = materialNew()
materialSetColor(floorLook, 120, 110, 100)
materialSetRoughness(floorLook, 0.9)
local crateLook = materialNew()
materialSetColor(crateLook, 200, 140, 60)
materialSetRoughness(crateLook, 0.7)
local ballLook = materialNew()
materialSetColor(ballLook, 60, 140, 230)
materialSetRoughness(ballLook, 0.3)
local paddleLook = materialNew()
materialSetColor(paddleLook, 230, 60, 40)
local screen = materialNew()
materialSetVideo(screen)
materialSetUnlit(screen, true)
local black = materialNew()
materialSetColor(black, 25, 25, 28)
local floor = nodeNew()
nodeSetMesh(floor, meshBox(14, 0.2, 14), floorLook)
nodeSetPosition(floor, 0, -1.9, 0)
bodyNew(floor, BODY_STATIC, SHAPE_BOX, 14, 0.2, 14)
-- A slab of a cabinet at the back with the disc on it, as a static body.
local cabinet = nodeNew()
nodeSetMesh(cabinet, meshBox(2.2, 3.0, 0.8), black)
nodeSetPosition(cabinet, 0, -0.3, -2.5)
bodyNew(cabinet, BODY_STATIC, SHAPE_BOX, 2.2, 3.0, 0.8)
local tv = nodeNew(cabinet)
nodeSetMesh(tv, meshPlane(1.8, 1.35), screen)
nodeSetPosition(tv, 0, 0.5, 0.41)
nodeSetRotation(tv, 90, 0, 0)
local crates = {}
for i = 1, 6 do
local crate = nodeNew()
nodeSetMesh(crate, meshBox(0.6, 0.6, 0.6), crateLook)
nodeSetPosition(crate, -0.6 + (i % 2) * 0.05, -1.5 + (i - 1) * 0.62, 0.2)
nodeSetRotation(crate, 0, (i - 1) * 7, 0)
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.6, 0.6, 0.6)
bodySetFriction(crate, 0.6)
crates[i] = crate
end
local ball = nodeNew()
nodeSetMesh(ball, meshSphere(0.35, 32), ballLook)
nodeSetPosition(ball, 1.2, 3.0, 0.6)
bodyNew(ball, BODY_DYNAMIC, SHAPE_SPHERE, 0.35)
bodySetBounce(ball, 0.6)
bodySetMass(ball, 2)
local paddle = nodeNew()
nodeSetMesh(paddle, meshBox(0.3, 1.2, 1.6), paddleLook)
nodeSetPosition(paddle, -4.5, -1.2, 0.2)
bodyNew(paddle, BODY_KINEMATIC, SHAPE_BOX, 0.3, 1.2, 1.6)
local sun = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(sun, -3, 6, 4)
nodeLookAt(sun, 0, 0, 0)
lightSetIntensity(sun, 1.2)
lightSetShadow(sun, true)
local camera = nodeNew()
nodeSetPosition(camera, 3.5, 1.8, 6.5)
nodeLookAt(camera, 0, -0.4, 0)
cameraSet(camera)
cameraSetPerspective(50, 0.1, 100)
function onOverlayUpdate()
frames = frames + 1
overlayClear()
local bx, by, bz = nodeGetWorldPosition(ball)
fontPrint(20, 20, string.format("Physics %d ball y=%.1f %s", frames, by, bodyIsResting(ball) and "resting" or "moving"))
if frames >= 70 and frames < 160 then
-- The paddle sweeps through the stack from the left.
nodeMove(paddle, 0.07, 0, 0)
end
if frames == 40 or frames == 120 or frames == 200 then
singeScreenshot()
end
if frames == 210 then
singeQuit()
end
end

View file

@ -0,0 +1,25 @@
-- Stage 1 test: the 3D layer clears to translucent green over the playing disc; overlay text sits on top.
local font = fontLoad("Singe/FreeSansBold.ttf", 28)
local frames = 0
fontSelect(font)
discPlay()
function onOverlayUpdate()
frames = frames + 1
overlayClear()
fontPrint(20, 20, "Overlay text above the scene, frame " .. frames)
if frames == 30 then
sceneEnable(true)
sceneSetBackground(0, 255, 0, 96)
end
if frames == 20 then
singeScreenshot()
end
if frames == 90 then
singeScreenshot()
end
if frames == 150 then
singeQuit()
end
end

View file

@ -0,0 +1,501 @@
cmake_minimum_required(VERSION 3.20 FATAL_ERROR)
project(JoltPhysics VERSION 5.6.0 LANGUAGES CXX)
# When turning this option on, the library will be compiled using assertions. By default asserts are enabled in Debug build.
option(USE_ASSERTS "Enable asserts" OFF)
# When turning this option on, the library will be compiled using doubles for positions. This allows for much bigger worlds.
option(DOUBLE_PRECISION "Use double precision math" OFF)
# When turning this option on, the library will be compiled with debug symbols
option(GENERATE_DEBUG_SYMBOLS "Generate debug symbols" ON)
# Which type of debug symbols to generate, e.g. using source-map when compiling with emscripten makes compilation a lot faster
set(JPH_DEBUG_SYMBOL_FORMAT "" CACHE STRING "Which type of debug symbols to generate")
# When turning this option on, the library will override the default CMAKE_CXX_FLAGS_DEBUG/RELEASE values, otherwise they will use the platform defaults
option(OVERRIDE_CXX_FLAGS "Override CMAKE_CXX_FLAGS_DEBUG/RELEASE" ON)
# When turning this option on, the library will be compiled in such a way to attempt to keep the simulation deterministic across platforms
option(CROSS_PLATFORM_DETERMINISTIC "Cross platform deterministic" OFF)
# When turning this option on, the library will be compiled for ARM using the CROSS_COMPILE_ARM_TARGET architecture, requires compiling with clang
option(CROSS_COMPILE_ARM "Cross compile to the CROSS_COMPILE_ARM_TARGET architecture" OFF)
# When cross compiling to ARM this specifies which target to use. Can be 'aarch64-linux-gnu' for 64-bit or 'arm-linux-gnueabihf' for 32-bit
set(CROSS_COMPILE_ARM_TARGET "aarch64-linux-gnu" CACHE STRING "The target to use")
# When turning this option on, Jolt will be compiled as a shared library and public symbols will be exported.
option(JPH_BUILD_SHARED_LIBS "Compile Jolt as a shared library" ${BUILD_SHARED_LIBS})
# When turning this option on, the library will be compiled with interprocedural optimizations enabled, also known as link-time optimizations or link-time code generation.
# Note that if you turn this on you need to use SET_INTERPROCEDURAL_OPTIMIZATION() or set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON) to enable LTO specifically for your own project as well.
# If you don't do this you may get an error: /usr/bin/ld: libJolt.a: error adding symbols: file format not recognized
option(INTERPROCEDURAL_OPTIMIZATION "Enable interprocedural optimizations" ON)
# When turning this on, in Debug and Release mode, the library will emit extra code to ensure that the 4th component of a 3-vector is kept the same as the 3rd component
# and will enable floating point exceptions during simulation to detect divisions by zero.
# Note that this currently only works using MSVC. Clang turns Float2 into a SIMD vector sometimes causing floating point exceptions (the option is ignored).
option(FLOATING_POINT_EXCEPTIONS_ENABLED "Enable floating point exceptions" ON)
# When turning this on, the library will be compiled with C++ exceptions enabled.
# This adds some overhead and Jolt doesn't use exceptions so by default it is off.
option(CPP_EXCEPTIONS_ENABLED "Enable C++ exceptions" OFF)
# When turning this on, the library will be compiled with C++ RTTI enabled.
# This adds some overhead and Jolt doesn't use RTTI so by default it is off.
option(CPP_RTTI_ENABLED "Enable C++ RTTI" OFF)
# Number of bits to use in ObjectLayer. Can be 16 or 32.
option(OBJECT_LAYER_BITS "Number of bits in ObjectLayer" 16)
# Select X86 processor features to use (if everything is off it will be SSE2 compatible)
option(USE_SSE4_1 "Enable SSE4.1" ON)
option(USE_SSE4_2 "Enable SSE4.2" ON)
option(USE_AVX "Enable AVX" ON)
option(USE_AVX2 "Enable AVX2" ON)
option(USE_AVX512 "Enable AVX512" OFF)
option(USE_LZCNT "Enable LZCNT" ON)
option(USE_TZCNT "Enable TZCNT" ON)
option(USE_F16C "Enable F16C" ON)
option(USE_FMADD "Enable FMADD" ON)
# Enable SIMD for the WASM build. Note that this is currently off by default since not all browsers support this.
# See: https://caniuse.com/?search=WebAssembly%20SIMD (Safari got support in March 2023 and was the last major browser to get support).
option(USE_WASM_SIMD "Enable SIMD for WASM" OFF)
# Enable 64 bit WASM instead of the default 32 bit WASM. Note that this currently requires special commandline flags in browsers and nodejs to enable.
# E.g. use 'node --experimental-wasm-memory64 UnitTests.js' to run the unit tests in nodejs in 64 bit.
option(JPH_USE_WASM64 "Enable 64 bit WASM" OFF)
# Enable all warnings
option(ENABLE_ALL_WARNINGS "Enable all warnings and warnings as errors" ON)
# Setting to periodically trace broadphase stats to help determine if the broadphase layer configuration is optimal
option(TRACK_BROADPHASE_STATS "Track Broadphase Stats" OFF)
# Setting to periodically trace narrowphase stats to help determine which collision queries could be optimized
option(TRACK_NARROWPHASE_STATS "Track Narrowphase Stats" OFF)
# Setting to track simulation timings per body
option(JPH_TRACK_SIMULATION_STATS "Track Simulation Stats" OFF)
# Enable the debug renderer in the Debug and Release builds. Note that DEBUG_RENDERER_IN_DISTRIBUTION will override this setting.
option(DEBUG_RENDERER_IN_DEBUG_AND_RELEASE "Enable debug renderer in Debug and Release builds" ON)
# Setting to enable the debug renderer in all builds.
# Note that enabling this reduces the performance of the library even if you're not drawing anything.
option(DEBUG_RENDERER_IN_DISTRIBUTION "Enable debug renderer in all builds" OFF)
# Enable the profiler in Debug and Release builds. Note that PROFILER_IN_DISTRIBUTION will override this setting.
option(PROFILER_IN_DEBUG_AND_RELEASE "Enable the profiler in Debug and Release builds" ON)
# Enable the profiler in all builds.
# Note that enabling this reduces the performance of the library.
option(PROFILER_IN_DISTRIBUTION "Enable the profiler in all builds" OFF)
# Ability to use the external profiler using CMake config. Defines preprocessor JPH_EXTERNAL_PROFILE.
# Use external profiler set using ProfileStartMeasurement and ProfileEndMeasurement to profile.
# Option is available only when profiling is enabled using PROFILER_IN_DEBUG_AND_RELEASE or PROFILER_IN_DISTRIBUTION.
option(JPH_USE_EXTERNAL_PROFILE "Use external profiler when profiling is enabled" OFF)
# Setting this option will force the library to use malloc/free instead of allowing the user to override the memory allocator
option(DISABLE_CUSTOM_ALLOCATOR "Disable support for a custom memory allocator" OFF)
# Setting this option will force the library to use the STL vector instead of the custom Array class
option(USE_STD_VECTOR "Use std::vector instead of own Array class" OFF)
# Setting this option will compile the ObjectStream class and RTTI attribute information
option(ENABLE_OBJECT_STREAM "Compile the ObjectStream class and RTTI attribute information" ON)
# Enable installation
option(ENABLE_INSTALL "Generate installation target" ON)
include(CMakeDependentOption)
# Ability to toggle between the static and DLL versions of the MSVC runtime library
# Windows Store only supports the DLL version
cmake_dependent_option(USE_STATIC_MSVC_RUNTIME_LIBRARY "Use the static MSVC runtime library" ON "MSVC;NOT WINDOWS_STORE" OFF)
# Option to compile with DirectX 12 compute
option(JPH_USE_DX12 "Use DirectX" ON)
# Option to compile with Vulkan compute
option(JPH_USE_VK "Use Vulkan" ON)
# Option to compile with Metal compute
option(JPH_USE_MTL "Use Metal" ON)
# Option to compile with CPU fallback compute
option(JPH_USE_CPU_COMPUTE "Use CPU Compute" ON)
# Option to enable shader debug symbols
option(JPH_SHADER_DEBUG_SYMBOLS "Shader Debug Symbols" OFF)
# Option to enable shader optimizations. When this is on, the shaders will be optimized for performance, otherwise they're suitable for debugging.
option(JPH_SHADER_OPTIMIZATION "Shader Optimization" ON)
# Determine which configurations exist
if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) # Only do this when we're at the top level, see: https://gitlab.kitware.com/cmake/cmake/-/issues/24181
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC" OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
set(CMAKE_CONFIGURATION_TYPES "Debug;Release;Distribution")
elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang" OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang")
set(CMAKE_CONFIGURATION_TYPES "Debug;Release;ReleaseASAN;ReleaseUBSAN;ReleaseTSAN;ReleaseCoverage;Distribution")
endif()
endif()
if (MSVC)
# 64 bit architecture
set(CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE "x64")
# Set runtime library
if (USE_STATIC_MSVC_RUNTIME_LIBRARY)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
endif()
# Set general compiler flags
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zc:__cplusplus /Gm- /MP /nologo /diagnostics:classic /FC /fp:except- /Zc:inline")
# Enable warnings
if (ENABLE_ALL_WARNINGS)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Wall /WX")
endif()
# Optionally generate debug symbols
if (GENERATE_DEBUG_SYMBOLS)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zi")
endif()
if (NOT CPP_RTTI_ENABLED)
# Set compiler flag for disabling RTTI
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /GR-")
else()
# Set compiler flag for enabling RTTI
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /GR")
endif()
if (NOT CPP_EXCEPTIONS_ENABLED)
# Remove any existing compiler flag that enables exceptions
string(REPLACE "/EHsc" "" CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS})
# Disable warning about STL and compiler-generated types using noexcept when exceptions are disabled
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4577")
else()
# Enable exceptions
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /EHsc")
endif()
# Set compiler flags for various configurations
if (OVERRIDE_CXX_FLAGS)
set(CMAKE_CXX_FLAGS_DEBUG "/GS /Od /Ob0 /RTC1")
set(CMAKE_CXX_FLAGS_RELEASE "/GS- /Gy /O2 /Oi /Ot")
endif()
set(CMAKE_CXX_FLAGS_DISTRIBUTION "${CMAKE_CXX_FLAGS_RELEASE}")
set(CMAKE_CXX_FLAGS_RELEASEASAN "-fsanitize=address /Od")
set(CMAKE_CXX_FLAGS_RELEASEUBSAN "-fsanitize=undefined,implicit-conversion,float-divide-by-zero,local-bounds -fno-sanitize-recover=all")
set(CMAKE_CXX_FLAGS_RELEASETSAN "${CMAKE_CXX_FLAGS_RELEASE} -fsanitize=thread")
set(CMAKE_CXX_FLAGS_RELEASECOVERAGE "-fprofile-instr-generate -fcoverage-mapping")
# Set linker flags
set(CMAKE_EXE_LINKER_FLAGS "/SUBSYSTEM:WINDOWS /ignore:4221")
if (GENERATE_DEBUG_SYMBOLS)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /DEBUG")
endif()
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC")
if (CROSS_PLATFORM_DETERMINISTIC)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /fp:precise")
else()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /fp:fast") # Clang doesn't use fast math because it cannot be turned off inside a single compilation unit
endif()
elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
# Fill in the path to the asan libraries
string(REGEX MATCH "^[0-9]+" CLANG_VERSION_MAJOR "${CMAKE_CXX_COMPILER_VERSION}")
set(CLANG_LIB_PATH "\"$(VSInstallDir)\\VC\\Tools\\Llvm\\x64\\lib\\clang\\${CLANG_VERSION_MAJOR}\\lib\\windows\"")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /showFilenames")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Qunused-arguments") # Clang emits warnings about unused arguments such as /MP and /GL
set(CMAKE_EXE_LINKER_FLAGS_RELEASEASAN "/SUBSYSTEM:CONSOLE /LIBPATH:${CLANG_LIB_PATH} clang_rt.asan_dynamic-x86_64.lib clang_rt.asan_dynamic_runtime_thunk-x86_64.lib")
set(CMAKE_EXE_LINKER_FLAGS_RELEASEUBSAN "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /LIBPATH:${CLANG_LIB_PATH}")
set(CMAKE_EXE_LINKER_FLAGS_RELEASETSAN "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /LIBPATH:${CLANG_LIB_PATH}")
set(CMAKE_EXE_LINKER_FLAGS_RELEASECOVERAGE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /LIBPATH:${CLANG_LIB_PATH}")
endif()
else()
# Enable warnings
if (ENABLE_ALL_WARNINGS)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Werror")
endif()
# Optionally generate debug symbols
if (GENERATE_DEBUG_SYMBOLS)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g${JPH_DEBUG_SYMBOL_FORMAT}")
endif()
if (NOT CPP_RTTI_ENABLED)
# Set compiler flag for disabling RTTI
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti")
else()
# Set compiler flag for enabling RTTI
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -frtti")
endif()
if (NOT CPP_EXCEPTIONS_ENABLED)
# Set compiler flag for disabling exception-handling
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions")
else()
# Set compiler flag for enabling exception-handling
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fexceptions")
endif()
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
# Also disable -Wstringop-overflow or it will generate false positives that can't be disabled from code when link-time optimizations are enabled
# Also disable -Wno-psabi to avoid messages of the form note: parameter passing for argument of type '...' changed in GCC 7.1
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-stringop-overflow -Wno-psabi")
else()
# Cross compiler flags
if (CROSS_COMPILE_ARM AND NOT ("${CROSS_COMPILE_ARM_TARGET}" STREQUAL ""))
set(CMAKE_CXX_FLAGS "--target=${CROSS_COMPILE_ARM_TARGET} ${CMAKE_CXX_FLAGS}")
endif()
endif()
# Do not use -ffast-math since it creates too many inaccuracies under clang
# FMA is not compatible with cross platform determinism
if (CROSS_PLATFORM_DETERMINISTIC OR NOT USE_FMADD OR EMSCRIPTEN)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -ffp-contract=off")
else()
if (${CMAKE_SYSTEM_PROCESSOR} MATCHES "^x86")
# On x86 our FMA intrinsics are preserved
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -ffp-contract=fast")
else()
# On ARM (and maybe other platforms) using 'fast' mode actually replaces a FMA intrinsic in Debug / when using LTO (?) with a 'mul' and an 'add' causing us to lose precision
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -ffp-contract=on")
endif()
endif()
# See https://github.com/jrouwe/JoltPhysics/issues/922. When compiling with DOUBLE_PRECISION=YES and CMAKE_OSX_DEPLOYMENT_TARGET=10.12 clang triggers a warning that we silence here.
if ("${CMAKE_SYSTEM_NAME}" MATCHES "Darwin" AND "${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -faligned-allocation")
endif()
# Set compiler flags for various configurations
if (OVERRIDE_CXX_FLAGS)
set(CMAKE_CXX_FLAGS_DEBUG "")
set(CMAKE_CXX_FLAGS_RELEASE "-O3")
endif()
set(CMAKE_CXX_FLAGS_DISTRIBUTION "${CMAKE_CXX_FLAGS_RELEASE}")
set(CMAKE_CXX_FLAGS_RELEASEASAN "-fsanitize=address")
set(CMAKE_CXX_FLAGS_RELEASEUBSAN "-fsanitize=undefined,implicit-conversion,float-divide-by-zero,local-bounds -fno-sanitize-recover=all")
set(CMAKE_CXX_FLAGS_RELEASETSAN "${CMAKE_CXX_FLAGS_RELEASE} -fsanitize=thread")
set(CMAKE_CXX_FLAGS_RELEASECOVERAGE "-O0 -DJPH_NO_FORCE_INLINE -fprofile-instr-generate -fcoverage-mapping")
endif()
# Set linker flags
set(CMAKE_EXE_LINKER_FLAGS_DISTRIBUTION "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
# Enable link time optimization in Release and Distribution mode if requested and available
function(SET_INTERPROCEDURAL_OPTIMIZATION)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE OFF PARENT_SCOPE)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_DISTRIBUTION OFF PARENT_SCOPE)
# On ARM, whole program optimization triggers an internal compiler error during code gen, so we don't turn it on
# When compiling as a shared lib with MinGW, turning on LTO causes errors of the form 'ld.exe: cannot export symbol X wrong type (4 vs 3)'
if (INTERPROCEDURAL_OPTIMIZATION
AND NOT ("${CMAKE_VS_PLATFORM_NAME}" STREQUAL "ARM64EC")
AND NOT ("${CMAKE_VS_PLATFORM_NAME}" STREQUAL "ARM64")
AND NOT ("${CMAKE_VS_PLATFORM_NAME}" STREQUAL "ARM")
AND (NOT CROSS_COMPILE_ARM OR ("${CROSS_COMPILE_ARM_TARGET}" STREQUAL "aarch64-linux-gnu"))
AND NOT (MINGW AND JPH_BUILD_SHARED_LIBS))
include(CheckIPOSupported)
check_ipo_supported(RESULT IS_IPO_SUPPORTED OUTPUT IPO_CHECK_OUTPUT)
if (IS_IPO_SUPPORTED)
message("Interprocedural optimizations are turned on")
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE ON PARENT_SCOPE)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_DISTRIBUTION ON PARENT_SCOPE)
else()
message("Warning: Interprocedural optimizations are not supported for this target, turn off the option INTERPROCEDURAL_OPTIMIZATION to disable this warning")
endif()
endif()
endfunction()
SET_INTERPROCEDURAL_OPTIMIZATION()
# Set repository root
set(PHYSICS_REPO_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../)
# Make Jolt Library
include(${PHYSICS_REPO_ROOT}/Jolt/Jolt.cmake)
if (XCODE)
# Ensure that we enable SSE4.2 for the x86_64 build, XCode builds multiple architectures
set_property(TARGET Jolt PROPERTY XCODE_ATTRIBUTE_OTHER_CPLUSPLUSFLAGS[arch=x86_64] "$(inherited) -msse4.2 -mpopcnt")
endif()
# Install Jolt library and includes
if (ENABLE_INSTALL)
include(GNUInstallDirs)
install(TARGETS Jolt
EXPORT JoltExport
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
foreach(SRC_FILE ${JOLT_PHYSICS_SRC_FILES})
string(REPLACE ${PHYSICS_REPO_ROOT} "" RELATIVE_SRC_FILE ${SRC_FILE})
get_filename_component(DESTINATION_PATH ${RELATIVE_SRC_FILE} DIRECTORY)
if (NOT RELATIVE_SRC_FILE MATCHES "\.cpp")
cmake_path(SET DST_FILE NORMALIZE "${CMAKE_INSTALL_INCLUDEDIR}/${DESTINATION_PATH}")
install(FILES ${SRC_FILE} DESTINATION ${DST_FILE})
endif()
endforeach()
foreach(SRC_FILE ${JOLT_PHYSICS_DXIL_SHADERS} ${JOLT_PHYSICS_METAL_LIB} ${JOLT_PHYSICS_SPV_SHADERS})
string(REPLACE ${PHYSICS_REPO_ROOT} "" RELATIVE_SRC_FILE ${SRC_FILE})
get_filename_component(DESTINATION_PATH ${RELATIVE_SRC_FILE} DIRECTORY)
cmake_path(SET DST_FILE NORMALIZE "${CMAKE_INSTALL_DATADIR}/${DESTINATION_PATH}")
install(FILES ${SRC_FILE} DESTINATION ${DST_FILE})
endforeach()
# Export Jolt library
export(TARGETS Jolt
NAMESPACE Jolt::
FILE JoltConfig.cmake)
install(EXPORT JoltExport
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/Jolt/
NAMESPACE Jolt::
FILE JoltConfig.cmake)
endif()
# Check if we're the root CMakeLists.txt, if not we are included by another CMake file and we should disable everything except for the main library
if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)
# Ability to turn ON/OFF individual applications
option(TARGET_UNIT_TESTS "Build Unit Tests" ON)
option(TARGET_HELLO_WORLD "Build Hello World" ON)
option(TARGET_PERFORMANCE_TEST "Build Performance Test" ON)
option(TARGET_SAMPLES "Build Samples" ON)
option(TARGET_VIEWER "Build JoltViewer" ON)
if (TARGET_UNIT_TESTS)
# Create UnitTests executable
include(${PHYSICS_REPO_ROOT}/UnitTests/UnitTests.cmake)
if (APPLE)
# Icon
set(JPH_ICON "${CMAKE_CURRENT_SOURCE_DIR}/macOS/icon.icns")
set_source_files_properties(${JPH_ICON} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
# macOS configuration
add_executable(UnitTests MACOSX_BUNDLE ${UNIT_TESTS_SRC_FILES} ${UNIT_TESTS_ASSETS} ${JPH_ICON})
# Make sure that all unit test assets move to the Resources folder in the package
foreach(ASSET_FILE ${UNIT_TESTS_ASSETS})
string(REPLACE ${PHYSICS_REPO_ROOT} "Resources" ASSET_DST ${ASSET_FILE})
get_filename_component(ASSET_DST ${ASSET_DST} DIRECTORY)
set_source_files_properties(${ASSET_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION ${ASSET_DST})
endforeach()
set_property(TARGET UnitTests PROPERTY MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/iOS/UnitTestsInfo.plist")
set_property(TARGET UnitTests PROPERTY XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.joltphysics.unittests")
set_property(TARGET UnitTests PROPERTY BUILD_RPATH "/usr/local/lib" INSTALL_RPATH "/usr/local/lib") # to find the Vulkan shared lib
# Ensure that we enable SSE4.2 for the x86_64 build, XCode builds multiple architectures
set_property(TARGET UnitTests PROPERTY XCODE_ATTRIBUTE_OTHER_CPLUSPLUSFLAGS[arch=x86_64] "$(inherited) -msse4.2 -mpopcnt")
# Unit tests are in the app bundle on macOS
set(UNIT_TEST_COMMAND UnitTests.app/Contents/MacOS/UnitTests)
else()
add_executable(UnitTests ${UNIT_TESTS_SRC_FILES})
set(UNIT_TEST_COMMAND UnitTests)
endif()
target_include_directories(UnitTests PUBLIC ${UNIT_TESTS_ROOT})
target_link_libraries(UnitTests LINK_PUBLIC Jolt)
if (EMSCRIPTEN)
target_link_options(UnitTests PUBLIC -sSTACK_SIZE=1048576 -sINITIAL_MEMORY=134217728)
endif()
# Code coverage doesn't work when using precompiled headers
if (CMAKE_GENERATOR STREQUAL "Ninja Multi-Config" AND MSVC)
# The Ninja Multi-Config generator errors out when selectively disabling precompiled headers for certain configurations.
# See: https://github.com/jrouwe/JoltPhysics/issues/1211
target_precompile_headers(UnitTests PRIVATE "${JOLT_PHYSICS_ROOT}/Jolt.h")
else()
target_precompile_headers(UnitTests PRIVATE "$<$<NOT:$<CONFIG:ReleaseCoverage>>:${JOLT_PHYSICS_ROOT}/Jolt.h>")
endif()
if (MSVC)
target_link_options(UnitTests PUBLIC "/SUBSYSTEM:CONSOLE")
endif()
# Register unit tests as a test so that it can be run with:
# ctest --output-on-failure
enable_testing()
add_test(UnitTests ${UNIT_TEST_COMMAND})
endif()
if (NOT "${CMAKE_SYSTEM_NAME}" STREQUAL "WindowsStore")
if (TARGET_HELLO_WORLD)
# Example 'Hello World' application
include(${PHYSICS_REPO_ROOT}/HelloWorld/HelloWorld.cmake)
add_executable(HelloWorld ${HELLO_WORLD_SRC_FILES})
target_include_directories(HelloWorld PUBLIC ${HELLO_WORLD_ROOT})
target_link_libraries(HelloWorld LINK_PUBLIC Jolt)
if (MSVC)
target_link_options(HelloWorld PUBLIC "/SUBSYSTEM:CONSOLE")
endif()
if (EMSCRIPTEN)
target_link_options(HelloWorld PUBLIC -sSTACK_SIZE=1048576 -sINITIAL_MEMORY=134217728)
endif()
endif()
if (TARGET_PERFORMANCE_TEST)
# Performance Test application
include(${PHYSICS_REPO_ROOT}/PerformanceTest/PerformanceTest.cmake)
add_executable(PerformanceTest ${PERFORMANCE_TEST_SRC_FILES})
target_include_directories(PerformanceTest PUBLIC ${PERFORMANCE_TEST_ROOT})
target_link_libraries(PerformanceTest LINK_PUBLIC Jolt)
if (MSVC)
target_link_options(PerformanceTest PUBLIC "/SUBSYSTEM:CONSOLE")
endif()
if (EMSCRIPTEN)
# Embed the assets for the RagdollScene
target_link_options(PerformanceTest PUBLIC "SHELL:--preload-file ${PHYSICS_REPO_ROOT}/Assets/Human.tof@/Assets/Human.tof")
target_link_options(PerformanceTest PUBLIC "SHELL:--preload-file ${PHYSICS_REPO_ROOT}/Assets/Human/dead_pose1.tof@/Assets/Human/dead_pose1.tof")
target_link_options(PerformanceTest PUBLIC "SHELL:--preload-file ${PHYSICS_REPO_ROOT}/Assets/terrain2.bof@/Assets/terrain2.bof")
target_link_options(PerformanceTest PUBLIC -sSTACK_SIZE=1048576 -sINITIAL_MEMORY=134217728)
endif()
set_property(TARGET PerformanceTest PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${PHYSICS_REPO_ROOT}")
endif()
endif()
if ((WIN32 OR LINUX OR ("${CMAKE_SYSTEM_NAME}" MATCHES "Darwin")) AND NOT ("${CMAKE_VS_PLATFORM_NAME}" STREQUAL "ARM")) # ARM 32-bit is missing dinput8.lib
if (TARGET_SAMPLES OR TARGET_VIEWER)
include(${PHYSICS_REPO_ROOT}/TestFramework/TestFramework.cmake)
endif()
if (TARGET_SAMPLES)
if (TEST_FRAMEWORK_AVAILABLE)
include(${PHYSICS_REPO_ROOT}/Samples/Samples.cmake)
else()
message("Cannot build Samples because Vulkan/DirectX/Metal SDK is not available!")
endif()
endif()
if (TARGET_VIEWER)
if (TEST_FRAMEWORK_AVAILABLE)
include(${PHYSICS_REPO_ROOT}/JoltViewer/JoltViewer.cmake)
else()
message("Cannot build JoltViewer because Vulkan/DirectX/Metal SDK is not available!")
endif()
endif()
endif()
# Copy DX12 runtime DLLs (dxcompiler.dll, dxil.dll) to the output directory of each application
if (JPH_DX12_RUNTIME_DLLS)
foreach(TARGET_NAME Samples JoltViewer UnitTests)
if (TARGET ${TARGET_NAME})
add_custom_command(TARGET ${TARGET_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${JPH_DX12_RUNTIME_DLLS}
$<TARGET_FILE_DIR:${TARGET_NAME}>
COMMENT "Copying DX12 runtime DLLs to ${TARGET_NAME} output directory")
endif()
endforeach()
endif()
endif()

View file

@ -0,0 +1,242 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <Jolt/Jolt.h>
#include <Jolt/AABBTree/AABBTreeBuilder.h>
JPH_NAMESPACE_BEGIN
uint AABBTreeBuilder::Node::GetMinDepth(const Array<Node> &inNodes) const
{
if (HasChildren())
{
uint left = inNodes[mChild[0]].GetMinDepth(inNodes);
uint right = inNodes[mChild[1]].GetMinDepth(inNodes);
return min(left, right) + 1;
}
else
return 1;
}
uint AABBTreeBuilder::Node::GetMaxDepth(const Array<Node> &inNodes) const
{
if (HasChildren())
{
uint left = inNodes[mChild[0]].GetMaxDepth(inNodes);
uint right = inNodes[mChild[1]].GetMaxDepth(inNodes);
return max(left, right) + 1;
}
else
return 1;
}
uint AABBTreeBuilder::Node::GetNodeCount(const Array<Node> &inNodes) const
{
if (HasChildren())
return inNodes[mChild[0]].GetNodeCount(inNodes) + inNodes[mChild[1]].GetNodeCount(inNodes) + 1;
else
return 1;
}
uint AABBTreeBuilder::Node::GetLeafNodeCount(const Array<Node> &inNodes) const
{
if (HasChildren())
return inNodes[mChild[0]].GetLeafNodeCount(inNodes) + inNodes[mChild[1]].GetLeafNodeCount(inNodes);
else
return 1;
}
uint AABBTreeBuilder::Node::GetTriangleCountInTree(const Array<Node> &inNodes) const
{
if (HasChildren())
return inNodes[mChild[0]].GetTriangleCountInTree(inNodes) + inNodes[mChild[1]].GetTriangleCountInTree(inNodes);
else
return GetTriangleCount();
}
void AABBTreeBuilder::Node::GetTriangleCountPerNode(const Array<Node> &inNodes, float &outAverage, uint &outMin, uint &outMax) const
{
outMin = INT_MAX;
outMax = 0;
outAverage = 0;
uint avg_divisor = 0;
GetTriangleCountPerNodeInternal(inNodes, outAverage, avg_divisor, outMin, outMax);
if (avg_divisor > 0)
outAverage /= avg_divisor;
}
float AABBTreeBuilder::Node::CalculateSAHCost(const Array<Node> &inNodes, float inCostTraversal, float inCostLeaf) const
{
float surface_area = mBounds.GetSurfaceArea();
return surface_area > 0.0f? CalculateSAHCostInternal(inNodes, inCostTraversal / surface_area, inCostLeaf / surface_area) : 0.0f;
}
void AABBTreeBuilder::Node::GetNChildren(const Array<Node> &inNodes, uint inN, Array<const Node*> &outChildren) const
{
JPH_ASSERT(outChildren.empty());
// Check if there is anything to expand
if (!HasChildren())
return;
// Start with the children of this node
outChildren.push_back(&inNodes[mChild[0]]);
outChildren.push_back(&inNodes[mChild[1]]);
size_t next = 0;
bool all_triangles = true;
while (outChildren.size() < inN)
{
// If we have looped over all nodes, start over with the first node again
if (next >= outChildren.size())
{
// If there only triangle nodes left, we have to terminate
if (all_triangles)
return;
next = 0;
all_triangles = true;
}
// Try to expand this node into its two children
const Node *to_expand = outChildren[next];
if (to_expand->HasChildren())
{
outChildren.erase(outChildren.begin() + next);
outChildren.push_back(&inNodes[to_expand->mChild[0]]);
outChildren.push_back(&inNodes[to_expand->mChild[1]]);
all_triangles = false;
}
else
{
++next;
}
}
}
float AABBTreeBuilder::Node::CalculateSAHCostInternal(const Array<Node> &inNodes, float inCostTraversalDivSurfaceArea, float inCostLeafDivSurfaceArea) const
{
if (HasChildren())
return inCostTraversalDivSurfaceArea * mBounds.GetSurfaceArea()
+ inNodes[mChild[0]].CalculateSAHCostInternal(inNodes, inCostTraversalDivSurfaceArea, inCostLeafDivSurfaceArea)
+ inNodes[mChild[1]].CalculateSAHCostInternal(inNodes, inCostTraversalDivSurfaceArea, inCostLeafDivSurfaceArea);
else
return inCostLeafDivSurfaceArea * mBounds.GetSurfaceArea() * GetTriangleCount();
}
void AABBTreeBuilder::Node::GetTriangleCountPerNodeInternal(const Array<Node> &inNodes, float &outAverage, uint &outAverageDivisor, uint &outMin, uint &outMax) const
{
if (HasChildren())
{
inNodes[mChild[0]].GetTriangleCountPerNodeInternal(inNodes, outAverage, outAverageDivisor, outMin, outMax);
inNodes[mChild[1]].GetTriangleCountPerNodeInternal(inNodes, outAverage, outAverageDivisor, outMin, outMax);
}
else
{
outAverage += GetTriangleCount();
outAverageDivisor++;
outMin = min(outMin, GetTriangleCount());
outMax = max(outMax, GetTriangleCount());
}
}
AABBTreeBuilder::AABBTreeBuilder(TriangleSplitter &inSplitter, uint inMaxTrianglesPerLeaf) :
mTriangleSplitter(inSplitter),
mMaxTrianglesPerLeaf(inMaxTrianglesPerLeaf)
{
}
AABBTreeBuilder::Node *AABBTreeBuilder::Build(AABBTreeBuilderStats &outStats)
{
TriangleSplitter::Range initial = mTriangleSplitter.GetInitialRange();
// Worst case for number of nodes: 1 leaf node per triangle. At each level above, the number of nodes is half that of the level below.
// This means that at most we'll be allocating 2x the number of triangles in nodes.
mNodes.reserve(2 * initial.Count());
mTriangles.reserve(initial.Count());
// Build the tree
Node &root = mNodes[BuildInternal(initial)];
// Collect stats
float avg_triangles_per_leaf;
uint min_triangles_per_leaf, max_triangles_per_leaf;
root.GetTriangleCountPerNode(mNodes, avg_triangles_per_leaf, min_triangles_per_leaf, max_triangles_per_leaf);
mTriangleSplitter.GetStats(outStats.mSplitterStats);
outStats.mSAHCost = root.CalculateSAHCost(mNodes, 1.0f, 1.0f);
outStats.mMinDepth = root.GetMinDepth(mNodes);
outStats.mMaxDepth = root.GetMaxDepth(mNodes);
outStats.mNodeCount = root.GetNodeCount(mNodes);
outStats.mLeafNodeCount = root.GetLeafNodeCount(mNodes);
outStats.mMaxTrianglesPerLeaf = mMaxTrianglesPerLeaf;
outStats.mTreeMinTrianglesPerLeaf = min_triangles_per_leaf;
outStats.mTreeMaxTrianglesPerLeaf = max_triangles_per_leaf;
outStats.mTreeAvgTrianglesPerLeaf = avg_triangles_per_leaf;
return &root;
}
uint AABBTreeBuilder::BuildInternal(const TriangleSplitter::Range &inTriangles)
{
// Check if there are too many triangles left
if (inTriangles.Count() > mMaxTrianglesPerLeaf)
{
// Split triangles in two batches
TriangleSplitter::Range left, right;
if (!mTriangleSplitter.Split(inTriangles, left, right))
{
// When the trace below triggers:
//
// This code builds a tree structure to accelerate collision detection.
// At top level it will start with all triangles in a mesh and then divides the triangles into two batches.
// This process repeats until until the batch size is smaller than mMaxTrianglePerLeaf.
//
// It uses a TriangleSplitter to find a good split. When this warning triggers, the splitter was not able
// to create a reasonable split for the triangles. This usually happens when the triangles in a batch are
// intersecting. They could also be overlapping when projected on the 3 coordinate axis.
//
// To solve this issue, you could try to pass your mesh through a mesh cleaning / optimization algorithm.
// You could also inspect the triangles that cause this issue and see if that part of the mesh can be fixed manually.
//
// When you do not fix this warning, the tree will be less efficient for collision detection, but it will still work.
JPH_IF_DEBUG(Trace("AABBTreeBuilder: Doing random split for %d triangles (max per node: %u)!", (int)inTriangles.Count(), mMaxTrianglesPerLeaf);)
int half = inTriangles.Count() / 2;
JPH_ASSERT(half > 0);
left = TriangleSplitter::Range(inTriangles.mBegin, inTriangles.mBegin + half);
right = TriangleSplitter::Range(inTriangles.mBegin + half, inTriangles.mEnd);
}
// Recursively build
const uint node_index = (uint)mNodes.size();
mNodes.push_back(Node());
uint left_index = BuildInternal(left);
uint right_index = BuildInternal(right);
Node &node = mNodes[node_index];
node.mChild[0] = left_index;
node.mChild[1] = right_index;
node.mBounds = mNodes[node.mChild[0]].mBounds;
node.mBounds.Encapsulate(mNodes[node.mChild[1]].mBounds);
return node_index;
}
// Create leaf node
const uint node_index = (uint)mNodes.size();
mNodes.push_back(Node());
Node &node = mNodes.back();
node.mTrianglesBegin = (uint)mTriangles.size();
node.mNumTriangles = inTriangles.mEnd - inTriangles.mBegin;
const VertexList &v = mTriangleSplitter.GetVertices();
for (uint i = inTriangles.mBegin; i < inTriangles.mEnd; ++i)
{
const IndexedTriangle &t = mTriangleSplitter.GetTriangle(i);
mTriangles.push_back(t);
node.mBounds.Encapsulate(v, t);
}
return node_index;
}
JPH_NAMESPACE_END

View file

@ -0,0 +1,121 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/TriangleSplitter/TriangleSplitter.h>
#include <Jolt/Geometry/AABox.h>
#include <Jolt/Core/NonCopyable.h>
JPH_NAMESPACE_BEGIN
struct AABBTreeBuilderStats
{
///@name Splitter stats
TriangleSplitter::Stats mSplitterStats; ///< Stats returned by the triangle splitter algorithm
///@name Tree structure
float mSAHCost = 0.0f; ///< Surface Area Heuristic cost of this tree
int mMinDepth = 0; ///< Minimal depth of tree (number of nodes)
int mMaxDepth = 0; ///< Maximum depth of tree (number of nodes)
int mNodeCount = 0; ///< Number of nodes in the tree
int mLeafNodeCount = 0; ///< Number of leaf nodes (that contain triangles)
///@name Configured stats
int mMaxTrianglesPerLeaf = 0; ///< Configured max triangles per leaf
///@name Actual stats
int mTreeMinTrianglesPerLeaf = 0; ///< Minimal amount of triangles in a leaf
int mTreeMaxTrianglesPerLeaf = 0; ///< Maximal amount of triangles in a leaf
float mTreeAvgTrianglesPerLeaf = 0.0f; ///< Average amount of triangles in leaf nodes
};
/// Helper class to build an AABB tree
class JPH_EXPORT AABBTreeBuilder
{
public:
/// A node in the tree, contains the AABox for the tree and any child nodes or triangles
class Node
{
public:
JPH_OVERRIDE_NEW_DELETE
/// Indicates that there is no child
static constexpr uint cInvalidNodeIndex = ~uint(0);
/// Get number of triangles in this node
inline uint GetTriangleCount() const { return mNumTriangles; }
/// Check if this node has any children
inline bool HasChildren() const { return mChild[0] != cInvalidNodeIndex || mChild[1] != cInvalidNodeIndex; }
/// Get child node
inline const Node * GetChild(uint inIdx, const Array<Node> &inNodes) const { return mChild[inIdx] != cInvalidNodeIndex? &inNodes[mChild[inIdx]] : nullptr; }
/// Min depth of tree
uint GetMinDepth(const Array<Node> &inNodes) const;
/// Max depth of tree
uint GetMaxDepth(const Array<Node> &inNodes) const;
/// Number of nodes in tree
uint GetNodeCount(const Array<Node> &inNodes) const;
/// Number of leaf nodes in tree
uint GetLeafNodeCount(const Array<Node> &inNodes) const;
/// Get triangle count in tree
uint GetTriangleCountInTree(const Array<Node> &inNodes) const;
/// Calculate min and max triangles per node
void GetTriangleCountPerNode(const Array<Node> &inNodes, float &outAverage, uint &outMin, uint &outMax) const;
/// Calculate the total cost of the tree using the surface area heuristic
float CalculateSAHCost(const Array<Node> &inNodes, float inCostTraversal, float inCostLeaf) const;
/// Recursively get children (breadth first) to get in total inN children (or less if there are no more)
void GetNChildren(const Array<Node> &inNodes, uint inN, Array<const Node *> &outChildren) const;
/// Bounding box
AABox mBounds;
/// Triangles (if no child nodes)
uint mTrianglesBegin; // Index into mTriangles
uint mNumTriangles = 0;
/// Child node indices (if no triangles)
uint mChild[2] = { cInvalidNodeIndex, cInvalidNodeIndex };
private:
friend class AABBTreeBuilder;
/// Recursive helper function to calculate cost of the tree
float CalculateSAHCostInternal(const Array<Node> &inNodes, float inCostTraversalDivSurfaceArea, float inCostLeafDivSurfaceArea) const;
/// Recursive helper function to calculate min and max triangles per node
void GetTriangleCountPerNodeInternal(const Array<Node> &inNodes, float &outAverage, uint &outAverageDivisor, uint &outMin, uint &outMax) const;
};
/// Constructor
explicit AABBTreeBuilder(TriangleSplitter &inSplitter, uint inMaxTrianglesPerLeaf = 16);
/// Recursively build tree, returns the root node of the tree
Node * Build(AABBTreeBuilderStats &outStats);
/// Get all nodes
const Array<Node> & GetNodes() const { return mNodes; }
/// Get all triangles
const Array<IndexedTriangle> &GetTriangles() const { return mTriangles; }
private:
uint BuildInternal(const TriangleSplitter::Range &inTriangles);
TriangleSplitter & mTriangleSplitter;
const uint mMaxTrianglesPerLeaf;
Array<Node> mNodes;
Array<IndexedTriangle> mTriangles;
};
JPH_NAMESPACE_END

View file

@ -0,0 +1,296 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/AABBTree/AABBTreeBuilder.h>
#include <Jolt/Core/ByteBuffer.h>
#include <Jolt/Geometry/IndexedTriangle.h>
JPH_NAMESPACE_BEGIN
/// Conversion algorithm that converts an AABB tree to an optimized binary buffer
template <class TriangleCodec, class NodeCodec>
class AABBTreeToBuffer
{
public:
/// Header for the tree
using NodeHeader = typename NodeCodec::Header;
/// Size in bytes of the header of the tree
static const int HeaderSize = NodeCodec::HeaderSize;
/// Maximum number of children per node in the tree
static const int NumChildrenPerNode = NodeCodec::NumChildrenPerNode;
/// Header for the triangles
using TriangleHeader = typename TriangleCodec::TriangleHeader;
/// Size in bytes of the header for the triangles
static const int TriangleHeaderSize = TriangleCodec::TriangleHeaderSize;
/// Convert AABB tree. Returns false if failed.
bool Convert(const Array<IndexedTriangle> &inTriangles, const Array<AABBTreeBuilder::Node> &inNodes, const VertexList &inVertices, const AABBTreeBuilder::Node *inRoot, bool inStoreUserData, const char *&outError)
{
typename NodeCodec::EncodingContext node_ctx;
typename TriangleCodec::EncodingContext tri_ctx(inVertices);
// Child nodes out of loop so we don't constantly realloc it
Array<const AABBTreeBuilder::Node *> child_nodes;
child_nodes.reserve(NumChildrenPerNode);
// First calculate how big the tree is going to be.
// Since the tree can be huge for very large meshes, we don't want
// to reallocate the buffer as it may cause out of memory situations.
// This loop mimics the construction loop below.
uint64 total_size = HeaderSize + TriangleHeaderSize;
size_t node_count = 1; // Start with root node
size_t to_process_max_size = 1; // Track size of queues so we can do a single reserve below
size_t to_process_triangles_max_size = 0;
{ // A scope to free the memory associated with to_estimate and to_estimate_triangles
Array<const AABBTreeBuilder::Node *> to_estimate;
Array<const AABBTreeBuilder::Node *> to_estimate_triangles;
to_estimate.push_back(inRoot);
for (;;)
{
while (!to_estimate.empty())
{
// Get the next node to process
const AABBTreeBuilder::Node *node = to_estimate.back();
to_estimate.pop_back();
// Update total size
node_ctx.PrepareNodeAllocate(node, total_size);
if (node->HasChildren())
{
// Collect the first NumChildrenPerNode sub-nodes in the tree
child_nodes.clear(); // Won't free the memory
node->GetNChildren(inNodes, NumChildrenPerNode, child_nodes);
// Increment the number of nodes we're going to store
node_count += child_nodes.size();
// Insert in reverse order so we estimate left child first when taking nodes from the back
for (int idx = int(child_nodes.size()) - 1; idx >= 0; --idx)
{
// Store triangles in separate list so we process them last
const AABBTreeBuilder::Node *child = child_nodes[idx];
if (child->HasChildren())
{
to_estimate.push_back(child);
to_process_max_size = max(to_estimate.size(), to_process_max_size);
}
else
{
to_estimate_triangles.push_back(child);
to_process_triangles_max_size = max(to_estimate_triangles.size(), to_process_triangles_max_size);
}
}
}
else
{
// Update total size
tri_ctx.PreparePack(&inTriangles[node->mTrianglesBegin], node->mNumTriangles, inStoreUserData, total_size);
}
}
// If we've got triangles to estimate, loop again with just the triangles
if (to_estimate_triangles.empty())
break;
else
to_estimate.swap(to_estimate_triangles);
}
}
// Finalize the prepare stage for the triangle context
tri_ctx.FinalizePreparePack(total_size);
// Reserve the buffer
if (size_t(total_size) != total_size)
{
outError = "AABBTreeToBuffer: Out of memory!";
return false;
}
mTree.reserve(size_t(total_size));
// Add headers
NodeHeader *header = HeaderSize > 0? mTree.Allocate<NodeHeader>() : nullptr;
TriangleHeader *triangle_header = TriangleHeaderSize > 0? mTree.Allocate<TriangleHeader>() : nullptr;
struct NodeData
{
const AABBTreeBuilder::Node * mNode = nullptr; // Node that this entry belongs to
Vec3 mNodeBoundsMin; // Quantized node bounds
Vec3 mNodeBoundsMax;
size_t mNodeStart = size_t(-1); // Start of node in mTree
size_t mTriangleStart = size_t(-1); // Start of the triangle data in mTree
size_t mChildNodeStart[NumChildrenPerNode]; // Start of the children of the node in mTree
size_t mChildTrianglesStart[NumChildrenPerNode]; // Start of the triangle data in mTree
size_t * mParentChildNodeStart = nullptr; // Where to store mNodeStart (to patch mChildNodeStart of my parent)
size_t * mParentTrianglesStart = nullptr; // Where to store mTriangleStart (to patch mChildTrianglesStart of my parent)
uint mNumChildren = 0; // Number of children
};
Array<NodeData *> to_process;
to_process.reserve(to_process_max_size);
Array<NodeData *> to_process_triangles;
to_process_triangles.reserve(to_process_triangles_max_size);
Array<NodeData> node_list;
node_list.reserve(node_count); // Needed to ensure that array is not reallocated, so we can keep pointers in the array
NodeData root;
root.mNode = inRoot;
root.mNodeBoundsMin = inRoot->mBounds.mMin;
root.mNodeBoundsMax = inRoot->mBounds.mMax;
node_list.push_back(root);
to_process.push_back(&node_list.back());
for (;;)
{
while (!to_process.empty())
{
// Get the next node to process
NodeData *node_data = to_process.back();
to_process.pop_back();
// Due to quantization box could have become bigger, not smaller
JPH_ASSERT(AABox(node_data->mNodeBoundsMin, node_data->mNodeBoundsMax).Contains(node_data->mNode->mBounds), "AABBTreeToBuffer: Bounding box became smaller!");
// Collect the first NumChildrenPerNode sub-nodes in the tree
child_nodes.clear(); // Won't free the memory
node_data->mNode->GetNChildren(inNodes, NumChildrenPerNode, child_nodes);
node_data->mNumChildren = (uint)child_nodes.size();
// Fill in default child bounds
Vec3 child_bounds_min[NumChildrenPerNode], child_bounds_max[NumChildrenPerNode];
for (size_t i = 0; i < NumChildrenPerNode; ++i)
if (i < child_nodes.size())
{
child_bounds_min[i] = child_nodes[i]->mBounds.mMin;
child_bounds_max[i] = child_nodes[i]->mBounds.mMax;
}
else
{
child_bounds_min[i] = Vec3::sZero();
child_bounds_max[i] = Vec3::sZero();
}
// Start a new node
node_data->mNodeStart = node_ctx.NodeAllocate(node_data->mNode, node_data->mNodeBoundsMin, node_data->mNodeBoundsMax, child_nodes, child_bounds_min, child_bounds_max, mTree, outError);
if (node_data->mNodeStart == size_t(-1))
return false;
if (node_data->mNode->HasChildren())
{
// Insert in reverse order so we process left child first when taking nodes from the back
for (int idx = int(child_nodes.size()) - 1; idx >= 0; --idx)
{
const AABBTreeBuilder::Node *child_node = child_nodes[idx];
// Due to quantization box could have become bigger, not smaller
JPH_ASSERT(AABox(child_bounds_min[idx], child_bounds_max[idx]).Contains(child_node->mBounds), "AABBTreeToBuffer: Bounding box became smaller!");
// Add child to list of nodes to be processed
NodeData child;
child.mNode = child_node;
child.mNodeBoundsMin = child_bounds_min[idx];
child.mNodeBoundsMax = child_bounds_max[idx];
child.mParentChildNodeStart = &node_data->mChildNodeStart[idx];
child.mParentTrianglesStart = &node_data->mChildTrianglesStart[idx];
node_list.push_back(child);
// Store triangles in separate list so we process them last
if (child_node->HasChildren())
to_process.push_back(&node_list.back());
else
to_process_triangles.push_back(&node_list.back());
}
}
else
{
// Add triangles
node_data->mTriangleStart = tri_ctx.Pack(&inTriangles[node_data->mNode->mTrianglesBegin], node_data->mNode->mNumTriangles, inStoreUserData, mTree, outError);
if (node_data->mTriangleStart == size_t(-1))
return false;
}
// Patch offset into parent
if (node_data->mParentChildNodeStart != nullptr)
{
*node_data->mParentChildNodeStart = node_data->mNodeStart;
*node_data->mParentTrianglesStart = node_data->mTriangleStart;
}
}
// If we've got triangles to process, loop again with just the triangles
if (to_process_triangles.empty())
break;
else
to_process.swap(to_process_triangles);
}
// Assert that our reservation was correct (we don't know if we swapped the arrays or not)
JPH_ASSERT(to_process_max_size == to_process.capacity() || to_process_triangles_max_size == to_process.capacity());
JPH_ASSERT(to_process_max_size == to_process_triangles.capacity() || to_process_triangles_max_size == to_process_triangles.capacity());
// Finalize all nodes
for (NodeData &n : node_list)
if (!node_ctx.NodeFinalize(n.mNode, n.mNodeStart, n.mNumChildren, n.mChildNodeStart, n.mChildTrianglesStart, mTree, outError))
return false;
// Finalize the triangles
tri_ctx.Finalize(inVertices, triangle_header, mTree);
// Validate that our reservations were correct
if (node_count != node_list.size())
{
outError = "Internal Error: Node memory estimate was incorrect, memory corruption!";
return false;
}
if (total_size != mTree.size())
{
outError = "Internal Error: Tree memory estimate was incorrect, memory corruption!";
return false;
}
// Finalize the nodes
return node_ctx.Finalize(header, inRoot, node_list[0].mNodeStart, node_list[0].mTriangleStart, outError);
}
/// Get resulting data
inline const ByteBuffer & GetBuffer() const
{
return mTree;
}
/// Get resulting data
inline ByteBuffer & GetBuffer()
{
return mTree;
}
/// Get header for tree
inline const NodeHeader * GetNodeHeader() const
{
return mTree.Get<NodeHeader>(0);
}
/// Get header for triangles
inline const TriangleHeader * GetTriangleHeader() const
{
return mTree.Get<TriangleHeader>(HeaderSize);
}
/// Get root of resulting tree
inline const void * GetRoot() const
{
return mTree.Get<void>(HeaderSize + TriangleHeaderSize);
}
private:
ByteBuffer mTree; ///< Resulting tree structure
};
JPH_NAMESPACE_END

View file

@ -0,0 +1,323 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/Core/ByteBuffer.h>
#include <Jolt/Math/HalfFloat.h>
#include <Jolt/AABBTree/AABBTreeBuilder.h>
JPH_NAMESPACE_BEGIN
class NodeCodecQuadTreeHalfFloat
{
public:
/// Number of child nodes of this node
static constexpr int NumChildrenPerNode = 4;
/// Header for the tree
struct Header
{
Float3 mRootBoundsMin;
Float3 mRootBoundsMax;
uint32 mRootProperties;
uint8 mBlockIDBits; ///< Number of bits to address a triangle block
uint8 mPadding[3] = { 0 };
};
/// Size of the header (an empty struct is always > 0 bytes so this needs a separate variable)
static constexpr int HeaderSize = sizeof(Header);
/// Stack size to use during DecodingContext::sWalkTree
static constexpr int StackSize = 128;
/// Node properties
enum : uint32
{
TRIANGLE_COUNT_BITS = 4,
TRIANGLE_COUNT_SHIFT = 28,
TRIANGLE_COUNT_MASK = (1 << TRIANGLE_COUNT_BITS) - 1,
OFFSET_BITS = 28,
OFFSET_MASK = (1 << OFFSET_BITS) - 1,
OFFSET_NON_SIGNIFICANT_BITS = 2,
OFFSET_NON_SIGNIFICANT_MASK = (1 << OFFSET_NON_SIGNIFICANT_BITS) - 1,
};
/// Node structure
struct Node
{
HalfFloat mBoundsMinX[4]; ///< 4 child bounding boxes
HalfFloat mBoundsMinY[4];
HalfFloat mBoundsMinZ[4];
HalfFloat mBoundsMaxX[4];
HalfFloat mBoundsMaxY[4];
HalfFloat mBoundsMaxZ[4];
uint32 mNodeProperties[4]; ///< 4 child node properties
};
static_assert(sizeof(Node) == 64, "Node should be 64 bytes");
/// This class encodes and compresses quad tree nodes
class EncodingContext
{
public:
/// Mimics the size a call to NodeAllocate() would add to the buffer
void PrepareNodeAllocate(const AABBTreeBuilder::Node *inNode, uint64 &ioBufferSize) const
{
// We don't emit nodes for leafs
if (!inNode->HasChildren())
return;
// Add size of node
ioBufferSize += sizeof(Node);
}
/// Allocate a new node for inNode.
/// Algorithm can modify the order of ioChildren to indicate in which order children should be compressed
/// Algorithm can enlarge the bounding boxes of the children during compression and returns these in outChildBoundsMin, outChildBoundsMax
/// inNodeBoundsMin, inNodeBoundsMax is the bounding box if inNode possibly widened by compressing the parent node
/// Returns size_t(-1) on error and reports the error in outError
size_t NodeAllocate(const AABBTreeBuilder::Node *inNode, Vec3Arg inNodeBoundsMin, Vec3Arg inNodeBoundsMax, Array<const AABBTreeBuilder::Node *> &ioChildren, Vec3 outChildBoundsMin[NumChildrenPerNode], Vec3 outChildBoundsMax[NumChildrenPerNode], ByteBuffer &ioBuffer, const char *&outError) const
{
// We don't emit nodes for leafs
if (!inNode->HasChildren())
return ioBuffer.size();
// Remember the start of the node
size_t node_start = ioBuffer.size();
// Fill in bounds
Node *node = ioBuffer.Allocate<Node>();
for (size_t i = 0; i < 4; ++i)
{
if (i < ioChildren.size())
{
const AABBTreeBuilder::Node *this_node = ioChildren[i];
// Copy bounding box
node->mBoundsMinX[i] = HalfFloatConversion::FromFloat<HalfFloatConversion::ROUND_TO_NEG_INF>(this_node->mBounds.mMin.GetX());
node->mBoundsMinY[i] = HalfFloatConversion::FromFloat<HalfFloatConversion::ROUND_TO_NEG_INF>(this_node->mBounds.mMin.GetY());
node->mBoundsMinZ[i] = HalfFloatConversion::FromFloat<HalfFloatConversion::ROUND_TO_NEG_INF>(this_node->mBounds.mMin.GetZ());
node->mBoundsMaxX[i] = HalfFloatConversion::FromFloat<HalfFloatConversion::ROUND_TO_POS_INF>(this_node->mBounds.mMax.GetX());
node->mBoundsMaxY[i] = HalfFloatConversion::FromFloat<HalfFloatConversion::ROUND_TO_POS_INF>(this_node->mBounds.mMax.GetY());
node->mBoundsMaxZ[i] = HalfFloatConversion::FromFloat<HalfFloatConversion::ROUND_TO_POS_INF>(this_node->mBounds.mMax.GetZ());
// Store triangle count
node->mNodeProperties[i] = this_node->GetTriangleCount() << TRIANGLE_COUNT_SHIFT;
if (this_node->GetTriangleCount() >= TRIANGLE_COUNT_MASK)
{
outError = "NodeCodecQuadTreeHalfFloat: Too many triangles";
return size_t(-1);
}
}
else
{
// Make this an invalid triangle node
node->mNodeProperties[i] = uint32(TRIANGLE_COUNT_MASK) << TRIANGLE_COUNT_SHIFT;
// Make bounding box invalid
node->mBoundsMinX[i] = HALF_FLT_MAX;
node->mBoundsMinY[i] = HALF_FLT_MAX;
node->mBoundsMinZ[i] = HALF_FLT_MAX;
node->mBoundsMaxX[i] = HALF_FLT_MAX;
node->mBoundsMaxY[i] = HALF_FLT_MAX;
node->mBoundsMaxZ[i] = HALF_FLT_MAX;
}
}
// Since we don't keep track of the bounding box while descending the tree, we keep the root bounds at all levels for triangle compression
for (int i = 0; i < NumChildrenPerNode; ++i)
{
outChildBoundsMin[i] = inNodeBoundsMin;
outChildBoundsMax[i] = inNodeBoundsMax;
}
return node_start;
}
/// Once all nodes have been added, this call finalizes all nodes by patching in the offsets of the child nodes (that were added after the node itself was added)
bool NodeFinalize(const AABBTreeBuilder::Node *inNode, size_t inNodeStart, uint inNumChildren, const size_t *inChildrenNodeStart, const size_t *inChildrenTrianglesStart, ByteBuffer &ioBuffer, const char *&outError)
{
if (!inNode->HasChildren())
return true;
Node *node = ioBuffer.Get<Node>(inNodeStart);
for (uint i = 0; i < inNumChildren; ++i)
{
size_t offset;
if (node->mNodeProperties[i] != 0)
{
// This is a triangle block
offset = inChildrenTrianglesStart[i];
// Store highest block with triangles so we can count the number of bits we need
mHighestTriangleBlock = max(mHighestTriangleBlock, offset);
}
else
{
// This is a node block
offset = inChildrenNodeStart[i];
}
// Store offset of next node / triangles
if (offset & OFFSET_NON_SIGNIFICANT_MASK)
{
outError = "NodeCodecQuadTreeHalfFloat: Internal Error: Offset has non-significant bits set";
return false;
}
offset >>= OFFSET_NON_SIGNIFICANT_BITS;
if (offset > OFFSET_MASK)
{
outError = "NodeCodecQuadTreeHalfFloat: Offset too large. Too much data.";
return false;
}
node->mNodeProperties[i] |= uint32(offset);
}
return true;
}
/// Once all nodes have been finalized, this will finalize the header of the nodes
bool Finalize(Header *outHeader, const AABBTreeBuilder::Node *inRoot, size_t inRootNodeStart, size_t inRootTrianglesStart, const char *&outError) const
{
// Check if we can address the root node
size_t offset = inRoot->HasChildren()? inRootNodeStart : inRootTrianglesStart;
if (offset & OFFSET_NON_SIGNIFICANT_MASK)
{
outError = "NodeCodecQuadTreeHalfFloat: Internal Error: Offset has non-significant bits set";
return false;
}
offset >>= OFFSET_NON_SIGNIFICANT_BITS;
if (offset > OFFSET_MASK)
{
outError = "NodeCodecQuadTreeHalfFloat: Offset too large. Too much data.";
return false;
}
// If the root has triangles, we need to take that offset instead since the mHighestTriangleBlock will be zero
size_t highest_triangle_block = inRootTrianglesStart != size_t(-1)? inRootTrianglesStart : mHighestTriangleBlock;
highest_triangle_block >>= OFFSET_NON_SIGNIFICANT_BITS;
inRoot->mBounds.mMin.StoreFloat3(&outHeader->mRootBoundsMin);
inRoot->mBounds.mMax.StoreFloat3(&outHeader->mRootBoundsMax);
outHeader->mRootProperties = uint32(offset) + (inRoot->GetTriangleCount() << TRIANGLE_COUNT_SHIFT);
outHeader->mBlockIDBits = uint8(32 - CountLeadingZeros(uint32(highest_triangle_block)));
if (inRoot->GetTriangleCount() >= TRIANGLE_COUNT_MASK)
{
outError = "NodeCodecQuadTreeHalfFloat: Too many triangles";
return false;
}
return true;
}
private:
size_t mHighestTriangleBlock = 0;
};
/// This class decodes and decompresses quad tree nodes
class DecodingContext
{
public:
/// Get the amount of bits needed to store an ID to a triangle block
inline static uint sTriangleBlockIDBits(const Header *inHeader)
{
return inHeader->mBlockIDBits;
}
/// Convert a triangle block ID to the start of the triangle buffer
inline static const void * sGetTriangleBlockStart(const uint8 *inBufferStart, uint inTriangleBlockID)
{
return inBufferStart + (inTriangleBlockID << OFFSET_NON_SIGNIFICANT_BITS);
}
/// Constructor
JPH_INLINE explicit DecodingContext(const Header *inHeader)
{
// Start with the root node on the stack
mNodeStack[0] = inHeader->mRootProperties;
}
/// Walk the node tree calling the Visitor::VisitNodes for each node encountered and Visitor::VisitTriangles for each triangle encountered
template <class TriangleContext, class Visitor>
JPH_INLINE void WalkTree(const uint8 *inBufferStart, const TriangleContext &inTriangleContext, Visitor &ioVisitor)
{
do
{
// Test if node contains triangles
uint32 node_properties = mNodeStack[mTop];
uint32 tri_count = node_properties >> TRIANGLE_COUNT_SHIFT;
if (tri_count == 0)
{
const Node *node = reinterpret_cast<const Node *>(inBufferStart + (node_properties << OFFSET_NON_SIGNIFICANT_BITS));
// Unpack bounds
#ifdef JPH_CPU_BIG_ENDIAN
Vec4 bounds_minx = HalfFloatConversion::ToFloat(UVec4(node->mBoundsMinX[0] + (node->mBoundsMinX[1] << 16), node->mBoundsMinX[2] + (node->mBoundsMinX[3] << 16), 0, 0));
Vec4 bounds_miny = HalfFloatConversion::ToFloat(UVec4(node->mBoundsMinY[0] + (node->mBoundsMinY[1] << 16), node->mBoundsMinY[2] + (node->mBoundsMinY[3] << 16), 0, 0));
Vec4 bounds_minz = HalfFloatConversion::ToFloat(UVec4(node->mBoundsMinZ[0] + (node->mBoundsMinZ[1] << 16), node->mBoundsMinZ[2] + (node->mBoundsMinZ[3] << 16), 0, 0));
Vec4 bounds_maxx = HalfFloatConversion::ToFloat(UVec4(node->mBoundsMaxX[0] + (node->mBoundsMaxX[1] << 16), node->mBoundsMaxX[2] + (node->mBoundsMaxX[3] << 16), 0, 0));
Vec4 bounds_maxy = HalfFloatConversion::ToFloat(UVec4(node->mBoundsMaxY[0] + (node->mBoundsMaxY[1] << 16), node->mBoundsMaxY[2] + (node->mBoundsMaxY[3] << 16), 0, 0));
Vec4 bounds_maxz = HalfFloatConversion::ToFloat(UVec4(node->mBoundsMaxZ[0] + (node->mBoundsMaxZ[1] << 16), node->mBoundsMaxZ[2] + (node->mBoundsMaxZ[3] << 16), 0, 0));
#else
UVec4 bounds_minxy = UVec4::sLoadInt4(reinterpret_cast<const uint32 *>(&node->mBoundsMinX[0]));
Vec4 bounds_minx = HalfFloatConversion::ToFloat(bounds_minxy);
Vec4 bounds_miny = HalfFloatConversion::ToFloat(bounds_minxy.Swizzle<SWIZZLE_Z, SWIZZLE_W, SWIZZLE_UNUSED, SWIZZLE_UNUSED>());
UVec4 bounds_minzmaxx = UVec4::sLoadInt4(reinterpret_cast<const uint32 *>(&node->mBoundsMinZ[0]));
Vec4 bounds_minz = HalfFloatConversion::ToFloat(bounds_minzmaxx);
Vec4 bounds_maxx = HalfFloatConversion::ToFloat(bounds_minzmaxx.Swizzle<SWIZZLE_Z, SWIZZLE_W, SWIZZLE_UNUSED, SWIZZLE_UNUSED>());
UVec4 bounds_maxyz = UVec4::sLoadInt4(reinterpret_cast<const uint32 *>(&node->mBoundsMaxY[0]));
Vec4 bounds_maxy = HalfFloatConversion::ToFloat(bounds_maxyz);
Vec4 bounds_maxz = HalfFloatConversion::ToFloat(bounds_maxyz.Swizzle<SWIZZLE_Z, SWIZZLE_W, SWIZZLE_UNUSED, SWIZZLE_UNUSED>());
#endif
// Load properties for 4 children
UVec4 properties = UVec4::sLoadInt4(&node->mNodeProperties[0]);
// Check which sub nodes to visit
int num_results = ioVisitor.VisitNodes(bounds_minx, bounds_miny, bounds_minz, bounds_maxx, bounds_maxy, bounds_maxz, properties, mTop);
// Push them onto the stack
JPH_ASSERT(mTop + 4 < StackSize);
properties.StoreInt4(&mNodeStack[mTop]);
mTop += num_results;
}
else if (tri_count != TRIANGLE_COUNT_MASK) // TRIANGLE_COUNT_MASK indicates a padding node, normally we shouldn't visit these nodes but when querying with a big enough box you could touch HALF_FLT_MAX (about 65K)
{
// Node contains triangles, do individual tests
uint32 triangle_block_id = node_properties & OFFSET_MASK;
const void *triangles = sGetTriangleBlockStart(inBufferStart, triangle_block_id);
ioVisitor.VisitTriangles(inTriangleContext, triangles, tri_count, triangle_block_id);
}
// Check if we're done
if (ioVisitor.ShouldAbort())
break;
// Fetch next node until we find one that the visitor wants to see
do
--mTop;
while (mTop >= 0 && !ioVisitor.ShouldVisitNode(mTop));
}
while (mTop >= 0);
}
/// This can be used to have the visitor early out (ioVisitor.ShouldAbort() returns true) and later continue again (call WalkTree() again)
bool IsDoneWalking() const
{
return mTop < 0;
}
private:
uint32 mNodeStack[StackSize];
int mTop = 0;
};
};
JPH_NAMESPACE_END

View file

@ -0,0 +1,555 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/Geometry/RayTriangle.h>
JPH_NAMESPACE_BEGIN
/// Store vertices in 64 bits and indices in 8 bits + 8 bit of flags per triangle like this:
///
/// TriangleBlockHeader,
/// TriangleBlock (4 triangles and their flags in 16 bytes),
/// TriangleBlock...
/// [Optional] UserData (4 bytes per triangle)
///
/// Vertices are stored:
///
/// VertexData (1 vertex in 64 bits),
/// VertexData...
///
/// They're compressed relative to the bounding box as provided by the node codec.
class TriangleCodecIndexed8BitPackSOA4Flags
{
public:
class TriangleHeader
{
public:
Float3 mOffset; ///< Offset of all vertices
Float3 mScale; ///< Scale of all vertices, vertex_position = mOffset + mScale * compressed_vertex_position
};
/// Size of the header (an empty struct is always > 0 bytes so this needs a separate variable)
static constexpr int TriangleHeaderSize = sizeof(TriangleHeader);
/// If this codec could return a different offset than the current buffer size when calling Pack()
static constexpr bool ChangesOffsetOnPack = false;
/// Amount of bits per component
enum EComponentData : uint32
{
COMPONENT_BITS = 21,
COMPONENT_MASK = (1 << COMPONENT_BITS) - 1,
};
/// Packed X and Y coordinate
enum EVertexXY : uint32
{
COMPONENT_X = 0,
COMPONENT_Y1 = COMPONENT_BITS,
COMPONENT_Y1_BITS = 32 - COMPONENT_BITS,
};
/// Packed Z and Y coordinate
enum EVertexZY : uint32
{
COMPONENT_Z = 0,
COMPONENT_Y2 = COMPONENT_BITS,
COMPONENT_Y2_BITS = 31 - COMPONENT_BITS,
};
/// A single packed vertex
struct VertexData
{
uint32 mVertexXY;
uint32 mVertexZY;
};
static_assert(sizeof(VertexData) == 8, "Compiler added padding");
/// A block of 4 triangles
struct TriangleBlock
{
uint8 mIndices[3][4]; ///< 8 bit indices to triangle vertices for 4 triangles in the form mIndices[vertex][triangle] where vertex in [0, 2] and triangle in [0, 3]
uint8 mFlags[4]; ///< Triangle flags (could contain material and active edges)
};
static_assert(sizeof(TriangleBlock) == 16, "Compiler added padding");
enum ETriangleBlockHeaderFlags : uint32
{
OFFSET_TO_VERTICES_BITS = 29, ///< Offset from current block to start of vertices in bytes
OFFSET_TO_VERTICES_MASK = (1 << OFFSET_TO_VERTICES_BITS) - 1,
OFFSET_NON_SIGNIFICANT_BITS = 2, ///< The offset from the current block to the start of the vertices must be a multiple of 4 bytes
OFFSET_NON_SIGNIFICANT_MASK = (1 << OFFSET_NON_SIGNIFICANT_BITS) - 1,
OFFSET_TO_USERDATA_BITS = 3, ///< When user data is stored, this is the number of blocks to skip to get to the user data (0 = no user data)
OFFSET_TO_USERDATA_MASK = (1 << OFFSET_TO_USERDATA_BITS) - 1,
};
/// A triangle header, will be followed by one or more TriangleBlocks
struct TriangleBlockHeader
{
const VertexData * GetVertexData() const { return reinterpret_cast<const VertexData *>(reinterpret_cast<const uint8 *>(this) + ((mFlags & OFFSET_TO_VERTICES_MASK) << OFFSET_NON_SIGNIFICANT_BITS)); }
const TriangleBlock * GetTriangleBlock() const { return reinterpret_cast<const TriangleBlock *>(reinterpret_cast<const uint8 *>(this) + sizeof(TriangleBlockHeader)); }
const uint32 * GetUserData() const { uint32 offset = mFlags >> OFFSET_TO_VERTICES_BITS; return offset == 0? nullptr : reinterpret_cast<const uint32 *>(GetTriangleBlock() + offset); }
uint32 mFlags;
};
static_assert(sizeof(TriangleBlockHeader) == 4, "Compiler added padding");
/// This class is used to validate that the triangle data will not be degenerate after compression
class ValidationContext
{
public:
/// Constructor
ValidationContext(const IndexedTriangleList &inTriangles, const VertexList &inVertices) :
mVertices(inVertices)
{
// Only used the referenced triangles, just like EncodingContext::Finalize does
for (const IndexedTriangle &i : inTriangles)
for (uint32 idx : i.mIdx)
mBounds.Encapsulate(Vec3(inVertices[idx]));
}
/// Test if a triangle will be degenerate after quantization
bool IsDegenerate(const IndexedTriangle &inTriangle) const
{
// Quantize the triangle in the same way as EncodingContext::Finalize does
UVec4 quantized_vertex[3];
Vec3 compress_scale = Vec3::sReplicate(COMPONENT_MASK) / Vec3::sMax(mBounds.GetSize(), Vec3::sReplicate(1.0e-20f));
for (int i = 0; i < 3; ++i)
quantized_vertex[i] = ((Vec3(mVertices[inTriangle.mIdx[i]]) - mBounds.mMin) * compress_scale + Vec3::sReplicate(0.5f)).ToInt();
return quantized_vertex[0] == quantized_vertex[1] || quantized_vertex[1] == quantized_vertex[2] || quantized_vertex[0] == quantized_vertex[2];
}
private:
const VertexList & mVertices;
AABox mBounds;
};
/// This class is used to encode and compress triangle data into a byte buffer
class EncodingContext
{
public:
/// Indicates a vertex hasn't been seen yet in the triangle list
static constexpr uint32 cNotFound = 0xffffffff;
/// Construct the encoding context
explicit EncodingContext(const VertexList &inVertices) :
mVertexMap(inVertices.size(), cNotFound)
{
}
/// Mimics the size a call to Pack() would add to the buffer
void PreparePack(const IndexedTriangle *inTriangles, uint inNumTriangles, bool inStoreUserData, uint64 &ioBufferSize)
{
// Add triangle block header
ioBufferSize += sizeof(TriangleBlockHeader);
// Compute first vertex that this batch will use (ensuring there's enough room if none of the vertices are shared)
uint start_vertex = Clamp((int)mVertexCount - 256 + (int)inNumTriangles * 3, 0, (int)mVertexCount);
// Pack vertices
uint padded_triangle_count = AlignUp(inNumTriangles, 4);
for (uint t = 0; t < padded_triangle_count; t += 4)
{
// Add triangle block header
ioBufferSize += sizeof(TriangleBlock);
for (uint vertex_nr = 0; vertex_nr < 3; ++vertex_nr)
for (uint block_tri_idx = 0; block_tri_idx < 4; ++block_tri_idx)
{
// Fetch vertex index. Create degenerate triangles for padding triangles.
bool triangle_available = t + block_tri_idx < inNumTriangles;
uint32 src_vertex_index = triangle_available? inTriangles[t + block_tri_idx].mIdx[vertex_nr] : inTriangles[inNumTriangles - 1].mIdx[0];
// Check if we've seen this vertex before and if it is in the range that we can encode
uint32 &vertex_index = mVertexMap[src_vertex_index];
if (vertex_index == cNotFound || vertex_index < start_vertex)
{
// Add vertex
vertex_index = mVertexCount;
mVertexCount++;
}
}
}
// Add user data
if (inStoreUserData)
ioBufferSize += inNumTriangles * sizeof(uint32);
}
/// Mimics the size the Finalize() call would add to ioBufferSize
void FinalizePreparePack(uint64 &ioBufferSize)
{
// Remember where the vertices are going to start in the output buffer
JPH_ASSERT(IsAligned(ioBufferSize, 4));
mVerticesStartIdx = size_t(ioBufferSize);
// Add vertices to buffer
ioBufferSize += uint64(mVertexCount) * sizeof(VertexData);
// Reserve the amount of memory we need for the vertices
mVertices.reserve(mVertexCount);
// Set vertex map back to 'not found'
for (uint32 &v : mVertexMap)
v = cNotFound;
}
/// Pack the triangles in inContainer to ioBuffer. This stores the mMaterialIndex of a triangle in the 8 bit flags.
/// Returns size_t(-1) on error.
size_t Pack(const IndexedTriangle *inTriangles, uint inNumTriangles, bool inStoreUserData, ByteBuffer &ioBuffer, const char *&outError)
{
JPH_ASSERT(inNumTriangles > 0);
// Determine position of triangles start
size_t triangle_block_start = ioBuffer.size();
// Allocate triangle block header
TriangleBlockHeader *header = ioBuffer.Allocate<TriangleBlockHeader>();
// Compute first vertex that this batch will use (ensuring there's enough room if none of the vertices are shared)
uint start_vertex = Clamp((int)mVertices.size() - 256 + (int)inNumTriangles * 3, 0, (int)mVertices.size());
// Store the start vertex offset relative to TriangleBlockHeader
size_t offset_to_vertices = mVerticesStartIdx - triangle_block_start + size_t(start_vertex) * sizeof(VertexData);
if (offset_to_vertices & OFFSET_NON_SIGNIFICANT_MASK)
{
outError = "TriangleCodecIndexed8BitPackSOA4Flags: Internal Error: Offset has non-significant bits set";
return size_t(-1);
}
offset_to_vertices >>= OFFSET_NON_SIGNIFICANT_BITS;
if (offset_to_vertices > OFFSET_TO_VERTICES_MASK)
{
outError = "TriangleCodecIndexed8BitPackSOA4Flags: Offset to vertices doesn't fit. Too much data.";
return size_t(-1);
}
header->mFlags = uint32(offset_to_vertices);
// When we store user data we need to store the offset to the user data in TriangleBlocks
uint padded_triangle_count = AlignUp(inNumTriangles, 4);
if (inStoreUserData)
{
uint32 num_blocks = padded_triangle_count >> 2;
JPH_ASSERT(num_blocks <= OFFSET_TO_USERDATA_MASK);
header->mFlags |= num_blocks << OFFSET_TO_VERTICES_BITS;
}
// Pack vertices
for (uint t = 0; t < padded_triangle_count; t += 4)
{
TriangleBlock *block = ioBuffer.Allocate<TriangleBlock>();
for (uint vertex_nr = 0; vertex_nr < 3; ++vertex_nr)
for (uint block_tri_idx = 0; block_tri_idx < 4; ++block_tri_idx)
{
// Fetch vertex index. Create degenerate triangles for padding triangles.
bool triangle_available = t + block_tri_idx < inNumTriangles;
uint32 src_vertex_index = triangle_available? inTriangles[t + block_tri_idx].mIdx[vertex_nr] : inTriangles[inNumTriangles - 1].mIdx[0];
// Check if we've seen this vertex before and if it is in the range that we can encode
uint32 &vertex_index = mVertexMap[src_vertex_index];
if (vertex_index == cNotFound || vertex_index < start_vertex)
{
// Add vertex
vertex_index = (uint32)mVertices.size();
mVertices.push_back(src_vertex_index);
}
// Store vertex index
uint32 vertex_offset = vertex_index - start_vertex;
if (vertex_offset > 0xff)
{
outError = "TriangleCodecIndexed8BitPackSOA4Flags: Offset doesn't fit in 8 bit";
return size_t(-1);
}
block->mIndices[vertex_nr][block_tri_idx] = (uint8)vertex_offset;
// Store flags
uint32 flags = triangle_available? inTriangles[t + block_tri_idx].mMaterialIndex : 0;
if (flags > 0xff)
{
outError = "TriangleCodecIndexed8BitPackSOA4Flags: Material index doesn't fit in 8 bit";
return size_t(-1);
}
block->mFlags[block_tri_idx] = (uint8)flags;
}
}
// Store user data
if (inStoreUserData)
{
uint32 *user_data = ioBuffer.Allocate<uint32>(inNumTriangles);
for (uint t = 0; t < inNumTriangles; ++t)
user_data[t] = inTriangles[t].mUserData;
}
return triangle_block_start;
}
/// After all triangles have been packed, this finalizes the header and triangle buffer
void Finalize(const VertexList &inVertices, TriangleHeader *ioHeader, ByteBuffer &ioBuffer) const
{
// Assert that our reservations were correct
JPH_ASSERT(mVertices.size() == mVertexCount);
JPH_ASSERT(ioBuffer.size() == mVerticesStartIdx);
// Check if anything to do
if (mVertices.empty())
return;
// Calculate bounding box
AABox bounds;
for (uint32 v : mVertices)
bounds.Encapsulate(Vec3(inVertices[v]));
// Compress vertices
VertexData *vertices = ioBuffer.Allocate<VertexData>(mVertices.size());
Vec3 compress_scale = Vec3::sReplicate(COMPONENT_MASK) / Vec3::sMax(bounds.GetSize(), Vec3::sReplicate(1.0e-20f));
for (uint32 v : mVertices)
{
UVec4 c = ((Vec3(inVertices[v]) - bounds.mMin) * compress_scale + Vec3::sReplicate(0.5f)).ToInt();
JPH_ASSERT(c.GetX() <= COMPONENT_MASK);
JPH_ASSERT(c.GetY() <= COMPONENT_MASK);
JPH_ASSERT(c.GetZ() <= COMPONENT_MASK);
vertices->mVertexXY = c.GetX() + (c.GetY() << COMPONENT_Y1);
vertices->mVertexZY = c.GetZ() + ((c.GetY() >> COMPONENT_Y1_BITS) << COMPONENT_Y2);
++vertices;
}
// Store decompression information
bounds.mMin.StoreFloat3(&ioHeader->mOffset);
(bounds.GetSize() / Vec3::sReplicate(COMPONENT_MASK)).StoreFloat3(&ioHeader->mScale);
}
private:
using VertexMap = Array<uint32>;
uint32 mVertexCount = 0; ///< Number of vertices calculated during PreparePack
size_t mVerticesStartIdx = 0; ///< Start of the vertices in the output buffer, calculated during PreparePack
Array<uint32> mVertices; ///< Output vertices as an index into the original vertex list (inVertices), sorted according to occurrence
VertexMap mVertexMap; ///< Maps from the original mesh vertex index (inVertices) to the index in our output vertices (mVertices)
};
/// This class is used to decode and decompress triangle data packed by the EncodingContext
class DecodingContext
{
private:
/// Private helper function to unpack the 1 vertex of 4 triangles (outX contains the x coordinate of triangle 0 .. 3 etc.)
JPH_INLINE void Unpack(const VertexData *inVertices, UVec4Arg inIndex, Vec4 &outX, Vec4 &outY, Vec4 &outZ) const
{
// Get compressed data
UVec4 c1 = UVec4::sGatherInt4<8>(&inVertices->mVertexXY, inIndex);
UVec4 c2 = UVec4::sGatherInt4<8>(&inVertices->mVertexZY, inIndex);
// Unpack the x y and z component
UVec4 xc = UVec4::sAnd(c1, UVec4::sReplicate(COMPONENT_MASK));
UVec4 yc = UVec4::sOr(c1.LogicalShiftRight<COMPONENT_Y1>(), c2.LogicalShiftRight<COMPONENT_Y2>().LogicalShiftLeft<COMPONENT_Y1_BITS>());
UVec4 zc = UVec4::sAnd(c2, UVec4::sReplicate(COMPONENT_MASK));
// Convert to float
outX = Vec4::sFusedMultiplyAdd(xc.ToFloat(), mScaleX, mOffsetX);
outY = Vec4::sFusedMultiplyAdd(yc.ToFloat(), mScaleY, mOffsetY);
outZ = Vec4::sFusedMultiplyAdd(zc.ToFloat(), mScaleZ, mOffsetZ);
}
/// Private helper function to unpack 4 triangles from a triangle block
JPH_INLINE void Unpack(const TriangleBlock *inBlock, const VertexData *inVertices, Vec4 &outX1, Vec4 &outY1, Vec4 &outZ1, Vec4 &outX2, Vec4 &outY2, Vec4 &outZ2, Vec4 &outX3, Vec4 &outY3, Vec4 &outZ3) const
{
// Get the indices for the three vertices (reads 4 bytes extra, but these are the flags so that's ok)
UVec4 indices = UVec4::sLoadInt4(reinterpret_cast<const uint32 *>(&inBlock->mIndices[0]));
UVec4 iv1 = indices.Expand4Byte0();
UVec4 iv2 = indices.Expand4Byte4();
UVec4 iv3 = indices.Expand4Byte8();
#ifdef JPH_CPU_BIG_ENDIAN
// On big endian systems we need to reverse the bytes
iv1 = iv1.Swizzle<SWIZZLE_W, SWIZZLE_Z, SWIZZLE_Y, SWIZZLE_X>();
iv2 = iv2.Swizzle<SWIZZLE_W, SWIZZLE_Z, SWIZZLE_Y, SWIZZLE_X>();
iv3 = iv3.Swizzle<SWIZZLE_W, SWIZZLE_Z, SWIZZLE_Y, SWIZZLE_X>();
#endif
// Decompress the triangle data
Unpack(inVertices, iv1, outX1, outY1, outZ1);
Unpack(inVertices, iv2, outX2, outY2, outZ2);
Unpack(inVertices, iv3, outX3, outY3, outZ3);
}
public:
JPH_INLINE explicit DecodingContext(const TriangleHeader *inHeader) :
mOffsetX(Vec4::sReplicate(inHeader->mOffset.x)),
mOffsetY(Vec4::sReplicate(inHeader->mOffset.y)),
mOffsetZ(Vec4::sReplicate(inHeader->mOffset.z)),
mScaleX(Vec4::sReplicate(inHeader->mScale.x)),
mScaleY(Vec4::sReplicate(inHeader->mScale.y)),
mScaleZ(Vec4::sReplicate(inHeader->mScale.z))
{
}
/// Unpacks triangles in the format t1v1,t1v2,t1v3, t2v1,t2v2,t2v3, ...
JPH_INLINE void Unpack(const void *inTriangleStart, uint32 inNumTriangles, Vec3 *outTriangles) const
{
JPH_ASSERT(inNumTriangles > 0);
const TriangleBlockHeader *header = reinterpret_cast<const TriangleBlockHeader *>(inTriangleStart);
const VertexData *vertices = header->GetVertexData();
const TriangleBlock *t = header->GetTriangleBlock();
const TriangleBlock *end = t + ((inNumTriangles + 3) >> 2);
int triangles_left = inNumTriangles;
do
{
// Unpack the vertices for 4 triangles
Vec4 v1x, v1y, v1z, v2x, v2y, v2z, v3x, v3y, v3z;
Unpack(t, vertices, v1x, v1y, v1z, v2x, v2y, v2z, v3x, v3y, v3z);
// Transpose it so we get normal vectors
Mat44 v1 = Mat44(v1x, v1y, v1z, Vec4::sZero()).Transposed();
Mat44 v2 = Mat44(v2x, v2y, v2z, Vec4::sZero()).Transposed();
Mat44 v3 = Mat44(v3x, v3y, v3z, Vec4::sZero()).Transposed();
// Store triangle data
for (int i = 0; i < 4 && triangles_left > 0; ++i, --triangles_left)
{
*outTriangles++ = v1.GetColumn3(i);
*outTriangles++ = v2.GetColumn3(i);
*outTriangles++ = v3.GetColumn3(i);
}
++t;
}
while (t < end);
}
/// Tests a ray against the packed triangles
JPH_INLINE float TestRay(Vec3Arg inRayOrigin, Vec3Arg inRayDirection, const void *inTriangleStart, uint32 inNumTriangles, float inClosest, uint32 &outClosestTriangleIndex) const
{
JPH_ASSERT(inNumTriangles > 0);
const TriangleBlockHeader *header = reinterpret_cast<const TriangleBlockHeader *>(inTriangleStart);
const VertexData *vertices = header->GetVertexData();
const TriangleBlock *t = header->GetTriangleBlock();
const TriangleBlock *end = t + ((inNumTriangles + 3) >> 2);
Vec4 closest = Vec4::sReplicate(inClosest);
UVec4 closest_triangle_idx = UVec4::sZero();
UVec4 start_triangle_idx = UVec4::sZero();
do
{
// Unpack the vertices for 4 triangles
Vec4 v1x, v1y, v1z, v2x, v2y, v2z, v3x, v3y, v3z;
Unpack(t, vertices, v1x, v1y, v1z, v2x, v2y, v2z, v3x, v3y, v3z);
// Perform ray vs triangle test
Vec4 distance = RayTriangle4(inRayOrigin, inRayDirection, v1x, v1y, v1z, v2x, v2y, v2z, v3x, v3y, v3z);
// Update closest with the smaller values
UVec4 smaller = Vec4::sLess(distance, closest);
closest = Vec4::sSelect(closest, distance, smaller);
// Update triangle index with the smallest values
UVec4 triangle_idx = start_triangle_idx + UVec4(0, 1, 2, 3);
closest_triangle_idx = UVec4::sSelect(closest_triangle_idx, triangle_idx, smaller);
// Next block
++t;
start_triangle_idx += UVec4::sReplicate(4);
}
while (t < end);
// Get the smallest component
Vec4::sSort4(closest, closest_triangle_idx);
outClosestTriangleIndex = closest_triangle_idx.GetX();
return closest.GetX();
}
/// Decode a single triangle
inline void GetTriangle(const void *inTriangleStart, uint32 inTriangleIdx, Vec3 &outV1, Vec3 &outV2, Vec3 &outV3) const
{
const TriangleBlockHeader *header = reinterpret_cast<const TriangleBlockHeader *>(inTriangleStart);
const VertexData *vertices = header->GetVertexData();
const TriangleBlock *block = header->GetTriangleBlock() + (inTriangleIdx >> 2);
uint32 block_triangle_idx = inTriangleIdx & 0b11;
// Get the 3 vertices
const VertexData &v1 = vertices[block->mIndices[0][block_triangle_idx]];
const VertexData &v2 = vertices[block->mIndices[1][block_triangle_idx]];
const VertexData &v3 = vertices[block->mIndices[2][block_triangle_idx]];
// Pack the vertices
UVec4 c1(v1.mVertexXY, v2.mVertexXY, v3.mVertexXY, 0);
UVec4 c2(v1.mVertexZY, v2.mVertexZY, v3.mVertexZY, 0);
// Unpack the x y and z component
UVec4 xc = UVec4::sAnd(c1, UVec4::sReplicate(COMPONENT_MASK));
UVec4 yc = UVec4::sOr(c1.LogicalShiftRight<COMPONENT_Y1>(), c2.LogicalShiftRight<COMPONENT_Y2>().LogicalShiftLeft<COMPONENT_Y1_BITS>());
UVec4 zc = UVec4::sAnd(c2, UVec4::sReplicate(COMPONENT_MASK));
// Convert to float
Vec4 vx = Vec4::sFusedMultiplyAdd(xc.ToFloat(), mScaleX, mOffsetX);
Vec4 vy = Vec4::sFusedMultiplyAdd(yc.ToFloat(), mScaleY, mOffsetY);
Vec4 vz = Vec4::sFusedMultiplyAdd(zc.ToFloat(), mScaleZ, mOffsetZ);
// Transpose it so we get normal vectors
Mat44 trans = Mat44(vx, vy, vz, Vec4::sZero()).Transposed();
outV1 = trans.GetAxisX();
outV2 = trans.GetAxisY();
outV3 = trans.GetAxisZ();
}
/// Get user data for a triangle
JPH_INLINE uint32 GetUserData(const void *inTriangleStart, uint32 inTriangleIdx) const
{
const TriangleBlockHeader *header = reinterpret_cast<const TriangleBlockHeader *>(inTriangleStart);
const uint32 *user_data = header->GetUserData();
return user_data != nullptr? user_data[inTriangleIdx] : 0;
}
/// Get flags for entire triangle block
JPH_INLINE static void sGetFlags(const void *inTriangleStart, uint32 inNumTriangles, uint8 *outTriangleFlags)
{
JPH_ASSERT(inNumTriangles > 0);
const TriangleBlockHeader *header = reinterpret_cast<const TriangleBlockHeader *>(inTriangleStart);
const TriangleBlock *t = header->GetTriangleBlock();
const TriangleBlock *end = t + ((inNumTriangles + 3) >> 2);
int triangles_left = inNumTriangles;
do
{
for (int i = 0; i < 4 && triangles_left > 0; ++i, --triangles_left)
*outTriangleFlags++ = t->mFlags[i];
++t;
}
while (t < end);
}
/// Get flags for a particular triangle
JPH_INLINE static uint8 sGetFlags(const void *inTriangleStart, int inTriangleIndex)
{
const TriangleBlockHeader *header = reinterpret_cast<const TriangleBlockHeader *>(inTriangleStart);
const TriangleBlock *first_block = header->GetTriangleBlock();
return first_block[inTriangleIndex >> 2].mFlags[inTriangleIndex & 0b11];
}
/// Unpacks triangles and flags, convenience function
JPH_INLINE void Unpack(const void *inTriangleStart, uint32 inNumTriangles, Vec3 *outTriangles, uint8 *outTriangleFlags) const
{
Unpack(inTriangleStart, inNumTriangles, outTriangles);
sGetFlags(inTriangleStart, inNumTriangles, outTriangleFlags);
}
private:
Vec4 mOffsetX;
Vec4 mOffsetY;
Vec4 mOffsetZ;
Vec4 mScaleX;
Vec4 mScaleY;
Vec4 mScaleZ;
};
};
JPH_NAMESPACE_END

View file

@ -0,0 +1,36 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2026 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <Jolt/Jolt.h>
#ifdef JPH_USE_CPU_COMPUTE
#include <Jolt/Compute/CPU/ComputeBufferCPU.h>
JPH_NAMESPACE_BEGIN
ComputeBufferCPU::ComputeBufferCPU(EType inType, uint64 inSize, uint inStride, const void *inData) :
ComputeBuffer(inType, inSize, inStride)
{
size_t buffer_size = size_t(mSize) * mStride;
mData = Allocate(buffer_size);
if (inData != nullptr)
memcpy(mData, inData, buffer_size);
}
ComputeBufferCPU::~ComputeBufferCPU()
{
Free(mData);
}
ComputeBufferResult ComputeBufferCPU::CreateReadBackBuffer() const
{
ComputeBufferResult result;
result.Set(const_cast<ComputeBufferCPU *>(this));
return result;
}
JPH_NAMESPACE_END
#endif // JPH_USE_CPU_COMPUTE

View file

@ -0,0 +1,36 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2026 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/Compute/ComputeBuffer.h>
#ifdef JPH_USE_CPU_COMPUTE
JPH_NAMESPACE_BEGIN
/// Buffer that can be used with the CPU compute system
class JPH_EXPORT ComputeBufferCPU final : public ComputeBuffer
{
public:
JPH_OVERRIDE_NEW_DELETE
/// Constructor / destructor
ComputeBufferCPU(EType inType, uint64 inSize, uint inStride, const void *inData);
virtual ~ComputeBufferCPU() override;
ComputeBufferResult CreateReadBackBuffer() const override;
void * GetData() const { return mData; }
private:
virtual void * MapInternal(EMode inMode) override { return mData; }
virtual void UnmapInternal() override { /* Nothing to do */ }
void * mData;
};
JPH_NAMESPACE_END
#endif // JPH_USE_CPU_COMPUTE

View file

@ -0,0 +1,101 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2026 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <Jolt/Jolt.h>
#ifdef JPH_USE_CPU_COMPUTE
#include <Jolt/Compute/CPU/ComputeQueueCPU.h>
#include <Jolt/Compute/CPU/ComputeShaderCPU.h>
#include <Jolt/Compute/CPU/ComputeBufferCPU.h>
#include <Jolt/Compute/CPU/ShaderWrapper.h>
#include <Jolt/Compute/CPU/HLSLToCPP.h>
JPH_NAMESPACE_BEGIN
ComputeQueueCPU::~ComputeQueueCPU()
{
JPH_ASSERT(mShader == nullptr && mWrapper == nullptr);
}
void ComputeQueueCPU::SetShader(const ComputeShader *inShader)
{
JPH_ASSERT(mShader == nullptr && mWrapper == nullptr);
mShader = static_cast<const ComputeShaderCPU *>(inShader);
mWrapper = mShader->CreateWrapper();
}
void ComputeQueueCPU::SetConstantBuffer(const char *inName, const ComputeBuffer *inBuffer)
{
if (inBuffer == nullptr)
return;
JPH_ASSERT(inBuffer->GetType() == ComputeBuffer::EType::ConstantBuffer);
const ComputeBufferCPU *buffer = static_cast<const ComputeBufferCPU *>(inBuffer);
mWrapper->Bind(inName, buffer->GetData(), buffer->GetSize() * buffer->GetStride());
mUsedBuffers.insert(buffer);
}
void ComputeQueueCPU::SetBuffer(const char *inName, const ComputeBuffer *inBuffer)
{
if (inBuffer == nullptr)
return;
JPH_ASSERT(inBuffer->GetType() == ComputeBuffer::EType::UploadBuffer || inBuffer->GetType() == ComputeBuffer::EType::Buffer || inBuffer->GetType() == ComputeBuffer::EType::RWBuffer);
const ComputeBufferCPU *buffer = static_cast<const ComputeBufferCPU *>(inBuffer);
mWrapper->Bind(inName, buffer->GetData(), buffer->GetSize() * buffer->GetStride());
mUsedBuffers.insert(buffer);
}
void ComputeQueueCPU::SetRWBuffer(const char *inName, ComputeBuffer *inBuffer, EBarrier inBarrier)
{
if (inBuffer == nullptr)
return;
JPH_ASSERT(inBuffer->GetType() == ComputeBuffer::EType::RWBuffer);
const ComputeBufferCPU *buffer = static_cast<const ComputeBufferCPU *>(inBuffer);
mWrapper->Bind(inName, buffer->GetData(), buffer->GetSize() * buffer->GetStride());
mUsedBuffers.insert(buffer);
}
void ComputeQueueCPU::ScheduleReadback(ComputeBuffer *inDst, const ComputeBuffer *inSrc)
{
/* Nothing to read back */
}
void ComputeQueueCPU::Dispatch(uint inThreadGroupsX, uint inThreadGroupsY, uint inThreadGroupsZ)
{
uint nx = inThreadGroupsX * mShader->GetGroupSizeX();
uint ny = inThreadGroupsY * mShader->GetGroupSizeY();
uint nz = inThreadGroupsZ * mShader->GetGroupSizeZ();
for (uint z = 0; z < nz; ++z)
for (uint y = 0; y < ny; ++y)
for (uint x = 0; x < nx; ++x)
{
HLSLToCPP::uint3 tid { x, y, z };
mWrapper->Main(tid);
}
delete mWrapper;
mWrapper = nullptr;
mUsedBuffers.clear();
mShader = nullptr;
}
void ComputeQueueCPU::Execute()
{
/* Nothing to do */
}
void ComputeQueueCPU::Wait()
{
/* Nothing to do */
}
JPH_NAMESPACE_END
#endif // JPH_USE_CPU_COMPUTE

View file

@ -0,0 +1,43 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2026 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/Compute/ComputeQueue.h>
#ifdef JPH_USE_CPU_COMPUTE
#include <Jolt/Compute/CPU/ComputeShaderCPU.h>
#include <Jolt/Core/UnorderedSet.h>
JPH_NAMESPACE_BEGIN
/// A command queue for the CPU compute system
class JPH_EXPORT ComputeQueueCPU final : public ComputeQueue
{
public:
JPH_OVERRIDE_NEW_DELETE
/// Destructor
virtual ~ComputeQueueCPU() override;
// See: ComputeQueue
virtual void SetShader(const ComputeShader *inShader) override;
virtual void SetConstantBuffer(const char *inName, const ComputeBuffer *inBuffer) override;
virtual void SetBuffer(const char *inName, const ComputeBuffer *inBuffer) override;
virtual void SetRWBuffer(const char *inName, ComputeBuffer *inBuffer, EBarrier inBarrier = EBarrier::Yes) override;
virtual void ScheduleReadback(ComputeBuffer *inDst, const ComputeBuffer *inSrc) override;
virtual void Dispatch(uint inThreadGroupsX, uint inThreadGroupsY, uint inThreadGroupsZ) override;
virtual void Execute() override;
virtual void Wait() override;
private:
RefConst<ComputeShaderCPU> mShader = nullptr; ///< Current active shader
ShaderWrapper * mWrapper = nullptr; ///< The active shader wrapper
UnorderedSet<RefConst<ComputeBuffer>> mUsedBuffers; ///< Buffers that are in use by the current execution, these will be retained until execution is finished so that we don't free buffers that are in use
};
JPH_NAMESPACE_END
#endif // JPH_USE_CPU_COMPUTE

View file

@ -0,0 +1,42 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2026 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/Compute/ComputeShader.h>
#ifdef JPH_USE_CPU_COMPUTE
JPH_NAMESPACE_BEGIN
class ShaderWrapper;
/// Compute shader handle for CPU compute
class JPH_EXPORT ComputeShaderCPU : public ComputeShader
{
public:
JPH_OVERRIDE_NEW_DELETE
using CreateShader = ShaderWrapper *(*)();
/// Constructor
ComputeShaderCPU(CreateShader inCreateShader, uint32 inGroupSizeX, uint32 inGroupSizeY, uint32 inGroupSizeZ) :
ComputeShader(inGroupSizeX, inGroupSizeY, inGroupSizeZ),
mCreateShader(inCreateShader)
{
}
/// Create an instance of the shader wrapper
ShaderWrapper * CreateWrapper() const
{
return mCreateShader();
}
private:
CreateShader mCreateShader;
};
JPH_NAMESPACE_END
#endif // JPH_USE_CPU_COMPUTE

View file

@ -0,0 +1,56 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2026 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <Jolt/Jolt.h>
#ifdef JPH_USE_CPU_COMPUTE
#include <Jolt/Compute/CPU/ComputeSystemCPU.h>
#include <Jolt/Compute/CPU/ComputeQueueCPU.h>
#include <Jolt/Compute/CPU/ComputeBufferCPU.h>
JPH_NAMESPACE_BEGIN
JPH_IMPLEMENT_RTTI_VIRTUAL(ComputeSystemCPU)
{
JPH_ADD_BASE_CLASS(ComputeSystemCPU, ComputeSystem)
}
ComputeShaderResult ComputeSystemCPU::CreateComputeShader(const char *inName, uint32 inGroupSizeX, uint32 inGroupSizeY, uint32 inGroupSizeZ)
{
ComputeShaderResult result;
const ShaderRegistry::const_iterator it = mShaderRegistry.find(inName);
if (it == mShaderRegistry.end())
{
result.SetError("Compute shader not found");
return result;
}
result.Set(new ComputeShaderCPU(it->second, inGroupSizeX, inGroupSizeY, inGroupSizeZ));
return result;
}
ComputeBufferResult ComputeSystemCPU::CreateComputeBuffer(ComputeBuffer::EType inType, uint64 inSize, uint inStride, const void *inData)
{
ComputeBufferResult result;
result.Set(new ComputeBufferCPU(inType, inSize, inStride, inData));
return result;
}
ComputeQueueResult ComputeSystemCPU::CreateComputeQueue()
{
ComputeQueueResult result;
result.Set(new ComputeQueueCPU());
return result;
}
ComputeSystemResult CreateComputeSystemCPU()
{
ComputeSystemResult result;
result.Set(new ComputeSystemCPU());
return result;
}
JPH_NAMESPACE_END
#endif // JPH_USE_CPU_COMPUTE

View file

@ -0,0 +1,52 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2026 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/Compute/ComputeSystem.h>
#ifdef JPH_USE_CPU_COMPUTE
#include <Jolt/Core/UnorderedMap.h>
#include <Jolt/Compute/CPU/ComputeShaderCPU.h>
JPH_NAMESPACE_BEGIN
/// Interface to run a workload on the CPU
/// This is intended mainly for debugging purposes and is not optimized for performance
class JPH_EXPORT ComputeSystemCPU : public ComputeSystem
{
public:
JPH_DECLARE_RTTI_VIRTUAL(JPH_EXPORT, ComputeSystemCPU)
// See: ComputeSystem
virtual ComputeShaderResult CreateComputeShader(const char *inName, uint32 inGroupSizeX, uint32 inGroupSizeY, uint32 inGroupSizeZ) override;
virtual ComputeBufferResult CreateComputeBuffer(ComputeBuffer::EType inType, uint64 inSize, uint inStride, const void *inData = nullptr) override;
virtual ComputeQueueResult CreateComputeQueue() override;
using CreateShader = ComputeShaderCPU::CreateShader;
void RegisterShader(const char *inName, CreateShader inCreateShader)
{
mShaderRegistry[inName] = inCreateShader;
}
private:
using ShaderRegistry = UnorderedMap<string_view, CreateShader>;
ShaderRegistry mShaderRegistry;
};
// Internal helpers
#define JPH_SHADER_WRAPPER_FUNCTION_NAME(name) RegisterShader##name
#define JPH_SHADER_WRAPPER_FUNCTION(sys, name) void JPH_EXPORT JPH_SHADER_WRAPPER_FUNCTION_NAME(name)(ComputeSystemCPU *sys)
/// Macro to declare a shader register function
#define JPH_DECLARE_REGISTER_SHADER(name) namespace JPH { class ComputeSystemCPU; JPH_SHADER_WRAPPER_FUNCTION(, name); }
/// Macro to register a shader
#define JPH_REGISTER_SHADER(sys, name) JPH::JPH_SHADER_WRAPPER_FUNCTION_NAME(name)(sys)
JPH_NAMESPACE_END
#endif // JPH_USE_CPU_COMPUTE

View file

@ -0,0 +1,525 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2026 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
JPH_NAMESPACE_BEGIN
/// Emulates HLSL vector types and operations in C++.
/// Note doesn't emulate things like barriers and group shared memory.
namespace HLSLToCPP {
using std::sqrt;
using std::min;
using std::max;
using std::round;
//////////////////////////////////////////////////////////////////////////////////////////
// float2
//////////////////////////////////////////////////////////////////////////////////////////
struct float2
{
// Constructors
inline float2() = default;
constexpr float2(float inX, float inY) : x(inX), y(inY) { }
explicit constexpr float2(float inS) : x(inS), y(inS) { }
// Operators
constexpr float2 & operator += (const float2 &inRHS) { x += inRHS.x; y += inRHS.y; return *this; }
constexpr float2 & operator -= (const float2 &inRHS) { x -= inRHS.x; y -= inRHS.y; return *this; }
constexpr float2 & operator *= (float inRHS) { x *= inRHS; y *= inRHS; return *this; }
constexpr float2 & operator /= (float inRHS) { x /= inRHS; y /= inRHS; return *this; }
constexpr float2 & operator *= (const float2 &inRHS) { x *= inRHS.x; y *= inRHS.y; return *this; }
constexpr float2 & operator /= (const float2 &inRHS) { x /= inRHS.x; y /= inRHS.y; return *this; }
// Equality
constexpr bool operator == (const float2 &inRHS) const { return x == inRHS.x && y == inRHS.y; }
constexpr bool operator != (const float2 &inRHS) const { return !(*this == inRHS); }
// Component access
const float & operator [] (uint inIndex) const { return (&x)[inIndex]; }
float & operator [] (uint inIndex) { return (&x)[inIndex]; }
// Swizzling (note return value is const to prevent assignment to swizzled results)
const float2 swizzle_xy() const { return float2(x, y); }
const float2 swizzle_yx() const { return float2(y, x); }
float x, y;
};
// Operators
constexpr float2 operator - (const float2 &inA) { return float2(-inA.x, -inA.y); }
constexpr float2 operator + (const float2 &inA, const float2 &inB) { return float2(inA.x + inB.x, inA.y + inB.y); }
constexpr float2 operator - (const float2 &inA, const float2 &inB) { return float2(inA.x - inB.x, inA.y - inB.y); }
constexpr float2 operator * (const float2 &inA, const float2 &inB) { return float2(inA.x * inB.x, inA.y * inB.y); }
constexpr float2 operator / (const float2 &inA, const float2 &inB) { return float2(inA.x / inB.x, inA.y / inB.y); }
constexpr float2 operator * (const float2 &inA, float inS) { return float2(inA.x * inS, inA.y * inS); }
constexpr float2 operator * (float inS, const float2 &inA) { return inA * inS; }
constexpr float2 operator / (const float2 &inA, float inS) { return float2(inA.x / inS, inA.y / inS); }
// Dot product
constexpr float dot(const float2 &inA, const float2 &inB) { return inA.x * inB.x + inA.y * inB.y; }
// Min value
constexpr float2 min(const float2 &inA, const float2 &inB) { return float2(min(inA.x, inB.x), min(inA.y, inB.y)); }
// Max value
constexpr float2 max(const float2 &inA, const float2 &inB) { return float2(max(inA.x, inB.x), max(inA.y, inB.y)); }
// Length
inline float length(const float2 &inV) { return sqrt(dot(inV, inV)); }
// Normalization
inline float2 normalize(const float2 &inV) { return inV / length(inV); }
// Rounding to int
inline float2 round(const float2 &inV) { return float2(round(inV.x), round(inV.y)); }
//////////////////////////////////////////////////////////////////////////////////////////
// float3
//////////////////////////////////////////////////////////////////////////////////////////
struct uint3;
struct float3
{
// Constructors
inline float3() = default;
constexpr float3(const float2 &inV, float inZ) : x(inV.x), y(inV.y), z(inZ) { }
constexpr float3(float inX, float inY, float inZ) : x(inX), y(inY), z(inZ) { }
explicit constexpr float3(float inS) : x(inS), y(inS), z(inS) { }
explicit constexpr float3(const uint3 &inV);
// Operators
constexpr float3 & operator += (const float3 &inRHS) { x += inRHS.x; y += inRHS.y; z += inRHS.z; return *this; }
constexpr float3 & operator -= (const float3 &inRHS) { x -= inRHS.x; y -= inRHS.y; z -= inRHS.z; return *this; }
constexpr float3 & operator *= (float inRHS) { x *= inRHS; y *= inRHS; z *= inRHS; return *this; }
constexpr float3 & operator /= (float inRHS) { x /= inRHS; y /= inRHS; z /= inRHS; return *this; }
constexpr float3 & operator *= (const float3 &inRHS) { x *= inRHS.x; y *= inRHS.y; z *= inRHS.z; return *this; }
constexpr float3 & operator /= (const float3 &inRHS) { x /= inRHS.x; y /= inRHS.y; z /= inRHS.z; return *this; }
// Equality
constexpr bool operator == (const float3 &inRHS) const { return x == inRHS.x && y == inRHS.y && z == inRHS.z; }
constexpr bool operator != (const float3 &inRHS) const { return !(*this == inRHS); }
// Component access
const float & operator [] (uint inIndex) const { return (&x)[inIndex]; }
float & operator [] (uint inIndex) { return (&x)[inIndex]; }
// Swizzling (note return value is const to prevent assignment to swizzled results)
const float2 swizzle_xy() const { return float2(x, y); }
const float2 swizzle_yx() const { return float2(y, x); }
const float3 swizzle_xyz() const { return float3(x, y, z); }
const float3 swizzle_xzy() const { return float3(x, z, y); }
const float3 swizzle_yxz() const { return float3(y, x, z); }
const float3 swizzle_yzx() const { return float3(y, z, x); }
const float3 swizzle_zxy() const { return float3(z, x, y); }
const float3 swizzle_zyx() const { return float3(z, y, x); }
float x, y, z;
};
// Operators
constexpr float3 operator - (const float3 &inA) { return float3(-inA.x, -inA.y, -inA.z); }
constexpr float3 operator + (const float3 &inA, const float3 &inB) { return float3(inA.x + inB.x, inA.y + inB.y, inA.z + inB.z); }
constexpr float3 operator - (const float3 &inA, const float3 &inB) { return float3(inA.x - inB.x, inA.y - inB.y, inA.z - inB.z); }
constexpr float3 operator * (const float3 &inA, const float3 &inB) { return float3(inA.x * inB.x, inA.y * inB.y, inA.z * inB.z); }
constexpr float3 operator / (const float3 &inA, const float3 &inB) { return float3(inA.x / inB.x, inA.y / inB.y, inA.z / inB.z); }
constexpr float3 operator * (const float3 &inA, float inS) { return float3(inA.x * inS, inA.y * inS, inA.z * inS); }
constexpr float3 operator * (float inS, const float3 &inA) { return inA * inS; }
constexpr float3 operator / (const float3 &inA, float inS) { return float3(inA.x / inS, inA.y / inS, inA.z / inS); }
// Dot product
constexpr float dot(const float3 &inA, const float3 &inB) { return inA.x * inB.x + inA.y * inB.y + inA.z * inB.z; }
// Min value
constexpr float3 min(const float3 &inA, const float3 &inB) { return float3(min(inA.x, inB.x), min(inA.y, inB.y), min(inA.z, inB.z)); }
// Max value
constexpr float3 max(const float3 &inA, const float3 &inB) { return float3(max(inA.x, inB.x), max(inA.y, inB.y), max(inA.z, inB.z)); }
// Length
inline float length(const float3 &inV) { return sqrt(dot(inV, inV)); }
// Normalization
inline float3 normalize(const float3 &inV) { return inV / length(inV); }
// Rounding to int
inline float3 round(const float3 &inV) { return float3(round(inV.x), round(inV.y), round(inV.z)); }
// Cross product
constexpr float3 cross(const float3 &inA, const float3 &inB) { return float3(inA.y * inB.z - inA.z * inB.y, inA.z * inB.x - inA.x * inB.z, inA.x * inB.y - inA.y * inB.x); }
//////////////////////////////////////////////////////////////////////////////////////////
// float4
//////////////////////////////////////////////////////////////////////////////////////////
struct int4;
struct float4
{
// Constructors
inline float4() = default;
constexpr float4(const float3 &inV, float inW) : x(inV.x), y(inV.y), z(inV.z), w(inW) { }
constexpr float4(float inX, float inY, float inZ, float inW) : x(inX), y(inY), z(inZ), w(inW) { }
explicit constexpr float4(float inS) : x(inS), y(inS), z(inS), w(inS) { }
explicit constexpr float4(const int4 &inV);
// Operators
constexpr float4 & operator += (const float4 &inRHS) { x += inRHS.x; y += inRHS.y; z += inRHS.z; w += inRHS.w; return *this; }
constexpr float4 & operator -= (const float4 &inRHS) { x -= inRHS.x; y -= inRHS.y; z -= inRHS.z; w -= inRHS.w; return *this; }
constexpr float4 & operator *= (float inRHS) { x *= inRHS; y *= inRHS; z *= inRHS; w *= inRHS; return *this; }
constexpr float4 & operator /= (float inRHS) { x /= inRHS; y /= inRHS; z /= inRHS; w /= inRHS; return *this; }
constexpr float4 & operator *= (const float4 &inRHS) { x *= inRHS.x; y *= inRHS.y; z *= inRHS.z; w *= inRHS.w; return *this; }
constexpr float4 & operator /= (const float4 &inRHS) { x /= inRHS.x; y /= inRHS.y; z /= inRHS.z; w /= inRHS.w; return *this; }
// Equality
constexpr bool operator == (const float4 &inRHS) const { return x == inRHS.x && y == inRHS.y && z == inRHS.z && w == inRHS.w; }
constexpr bool operator != (const float4 &inRHS) const { return !(*this == inRHS); }
// Component access
const float & operator [] (uint inIndex) const { return (&x)[inIndex]; }
float & operator [] (uint inIndex) { return (&x)[inIndex]; }
// Swizzling (note return value is const to prevent assignment to swizzled results)
const float2 swizzle_xy() const { return float2(x, y); }
const float2 swizzle_yx() const { return float2(y, x); }
const float3 swizzle_xyz() const { return float3(x, y, z); }
const float3 swizzle_xzy() const { return float3(x, z, y); }
const float3 swizzle_yxz() const { return float3(y, x, z); }
const float3 swizzle_yzx() const { return float3(y, z, x); }
const float3 swizzle_zxy() const { return float3(z, x, y); }
const float3 swizzle_zyx() const { return float3(z, y, x); }
const float4 swizzle_xywz() const { return float4(x, y, w, z); }
const float4 swizzle_xwyz() const { return float4(x, w, y, z); }
const float4 swizzle_wxyz() const { return float4(w, x, y, z); }
float x, y, z, w;
};
// Operators
constexpr float4 operator - (const float4 &inA) { return float4(-inA.x, -inA.y, -inA.z, -inA.w); }
constexpr float4 operator + (const float4 &inA, const float4 &inB) { return float4(inA.x + inB.x, inA.y + inB.y, inA.z + inB.z, inA.w + inB.w); }
constexpr float4 operator - (const float4 &inA, const float4 &inB) { return float4(inA.x - inB.x, inA.y - inB.y, inA.z - inB.z, inA.w - inB.w); }
constexpr float4 operator * (const float4 &inA, const float4 &inB) { return float4(inA.x * inB.x, inA.y * inB.y, inA.z * inB.z, inA.w * inB.w); }
constexpr float4 operator / (const float4 &inA, const float4 &inB) { return float4(inA.x / inB.x, inA.y / inB.y, inA.z / inB.z, inA.w / inB.w); }
constexpr float4 operator * (const float4 &inA, float inS) { return float4(inA.x * inS, inA.y * inS, inA.z * inS, inA.w * inS); }
constexpr float4 operator * (float inS, const float4 &inA) { return inA * inS; }
constexpr float4 operator / (const float4 &inA, float inS) { return float4(inA.x / inS, inA.y / inS, inA.z / inS, inA.w / inS); }
// Dot product
constexpr float dot(const float4 &inA, const float4 &inB) { return inA.x * inB.x + inA.y * inB.y + inA.z * inB.z + inA.w * inB.w; }
// Min value
constexpr float4 min(const float4 &inA, const float4 &inB) { return float4(min(inA.x, inB.x), min(inA.y, inB.y), min(inA.z, inB.z), min(inA.w, inB.w)); }
// Max value
constexpr float4 max(const float4 &inA, const float4 &inB) { return float4(max(inA.x, inB.x), max(inA.y, inB.y), max(inA.z, inB.z), max(inA.w, inB.w)); }
// Length
inline float length(const float4 &inV) { return sqrt(dot(inV, inV)); }
// Normalization
inline float4 normalize(const float4 &inV) { return inV / length(inV); }
// Rounding to int
inline float4 round(const float4 &inV) { return float4(round(inV.x), round(inV.y), round(inV.z), round(inV.w)); }
//////////////////////////////////////////////////////////////////////////////////////////
// uint3
//////////////////////////////////////////////////////////////////////////////////////////
struct uint3
{
inline uint3() = default;
constexpr uint3(uint32 inX, uint32 inY, uint32 inZ) : x(inX), y(inY), z(inZ) { }
explicit constexpr uint3(const float3 &inV) : x(uint32(inV.x)), y(uint32(inV.y)), z(uint32(inV.z)) { }
// Operators
constexpr uint3 & operator += (const uint3 &inRHS) { x += inRHS.x; y += inRHS.y; z += inRHS.z; return *this; }
constexpr uint3 & operator -= (const uint3 &inRHS) { x -= inRHS.x; y -= inRHS.y; z -= inRHS.z; return *this; }
constexpr uint3 & operator *= (uint32 inRHS) { x *= inRHS; y *= inRHS; z *= inRHS; return *this; }
constexpr uint3 & operator /= (uint32 inRHS) { x /= inRHS; y /= inRHS; z /= inRHS; return *this; }
constexpr uint3 & operator *= (const uint3 &inRHS) { x *= inRHS.x; y *= inRHS.y; z *= inRHS.z; return *this; }
constexpr uint3 & operator /= (const uint3 &inRHS) { x /= inRHS.x; y /= inRHS.y; z /= inRHS.z; return *this; }
// Equality
constexpr bool operator == (const uint3 &inRHS) const { return x == inRHS.x && y == inRHS.y && z == inRHS.z; }
constexpr bool operator != (const uint3 &inRHS) const { return !(*this == inRHS); }
// Component access
const uint32 & operator [] (uint inIndex) const { return (&x)[inIndex]; }
uint32 & operator [] (uint inIndex) { return (&x)[inIndex]; }
// Swizzling (note return value is const to prevent assignment to swizzled results)
const uint3 swizzle_xyz() const { return uint3(x, y, z); }
const uint3 swizzle_xzy() const { return uint3(x, z, y); }
const uint3 swizzle_yxz() const { return uint3(y, x, z); }
const uint3 swizzle_yzx() const { return uint3(y, z, x); }
const uint3 swizzle_zxy() const { return uint3(z, x, y); }
const uint3 swizzle_zyx() const { return uint3(z, y, x); }
uint32 x, y, z;
};
// Operators
constexpr uint3 operator + (const uint3 &inA, const uint3 &inB) { return uint3(inA.x + inB.x, inA.y + inB.y, inA.z + inB.z); }
constexpr uint3 operator - (const uint3 &inA, const uint3 &inB) { return uint3(inA.x - inB.x, inA.y - inB.y, inA.z - inB.z); }
constexpr uint3 operator * (const uint3 &inA, const uint3 &inB) { return uint3(inA.x * inB.x, inA.y * inB.y, inA.z * inB.z); }
constexpr uint3 operator / (const uint3 &inA, const uint3 &inB) { return uint3(inA.x / inB.x, inA.y / inB.y, inA.z / inB.z); }
constexpr uint3 operator * (const uint3 &inA, uint32 inS) { return uint3(inA.x * inS, inA.y * inS, inA.z * inS); }
constexpr uint3 operator * (uint32 inS, const uint3 &inA) { return inA * inS; }
constexpr uint3 operator / (const uint3 &inA, uint32 inS) { return uint3(inA.x / inS, inA.y / inS, inA.z / inS); }
// Dot product
constexpr uint32 dot(const uint3 &inA, const uint3 &inB) { return inA.x * inB.x + inA.y * inB.y + inA.z * inB.z; }
// Min value
constexpr uint3 min(const uint3 &inA, const uint3 &inB) { return uint3(min(inA.x, inB.x), min(inA.y, inB.y), min(inA.z, inB.z)); }
// Max value
constexpr uint3 max(const uint3 &inA, const uint3 &inB) { return uint3(max(inA.x, inB.x), max(inA.y, inB.y), max(inA.z, inB.z)); }
//////////////////////////////////////////////////////////////////////////////////////////
// uint4
//////////////////////////////////////////////////////////////////////////////////////////
struct uint4
{
// Constructors
inline uint4() = default;
constexpr uint4(const uint3 &inV, uint32 inW) : x(inV.x), y(inV.y), z(inV.z), w(inW) { }
constexpr uint4(uint32 inX, uint32 inY, uint32 inZ, uint32 inW) : x(inX), y(inY), z(inZ), w(inW) { }
explicit constexpr uint4(uint32 inS) : x(inS), y(inS), z(inS), w(inS) { }
// Operators
constexpr uint4 & operator += (const uint4 &inRHS) { x += inRHS.x; y += inRHS.y; z += inRHS.z; w += inRHS.w; return *this; }
constexpr uint4 & operator -= (const uint4 &inRHS) { x -= inRHS.x; y -= inRHS.y; z -= inRHS.z; w -= inRHS.w; return *this; }
constexpr uint4 & operator *= (uint32 inRHS) { x *= inRHS; y *= inRHS; z *= inRHS; w *= inRHS; return *this; }
constexpr uint4 & operator /= (uint32 inRHS) { x /= inRHS; y /= inRHS; z /= inRHS; w /= inRHS; return *this; }
constexpr uint4 & operator *= (const uint4 &inRHS) { x *= inRHS.x; y *= inRHS.y; z *= inRHS.z; w *= inRHS.w; return *this; }
constexpr uint4 & operator /= (const uint4 &inRHS) { x /= inRHS.x; y /= inRHS.y; z /= inRHS.z; w /= inRHS.w; return *this; }
// Equality
constexpr bool operator == (const uint4 &inRHS) const { return x == inRHS.x && y == inRHS.y && z == inRHS.z && w == inRHS.w; }
constexpr bool operator != (const uint4 &inRHS) const { return !(*this == inRHS); }
// Component access
const uint32 & operator [] (uint inIndex) const { return (&x)[inIndex]; }
uint32 & operator [] (uint inIndex) { return (&x)[inIndex]; }
// Swizzling (note return value is const to prevent assignment to swizzled results)
const uint3 swizzle_xyz() const { return uint3(x, y, z); }
const uint3 swizzle_xzy() const { return uint3(x, z, y); }
const uint3 swizzle_yxz() const { return uint3(y, x, z); }
const uint3 swizzle_yzx() const { return uint3(y, z, x); }
const uint3 swizzle_zxy() const { return uint3(z, x, y); }
const uint3 swizzle_zyx() const { return uint3(z, y, x); }
const uint4 swizzle_xywz() const { return uint4(x, y, w, z); }
const uint4 swizzle_xwyz() const { return uint4(x, w, y, z); }
const uint4 swizzle_wxyz() const { return uint4(w, x, y, z); }
uint32 x, y, z, w;
};
// Operators
constexpr uint4 operator + (const uint4 &inA, const uint4 &inB) { return uint4(inA.x + inB.x, inA.y + inB.y, inA.z + inB.z, inA.w + inB.w); }
constexpr uint4 operator - (const uint4 &inA, const uint4 &inB) { return uint4(inA.x - inB.x, inA.y - inB.y, inA.z - inB.z, inA.w - inB.w); }
constexpr uint4 operator * (const uint4 &inA, const uint4 &inB) { return uint4(inA.x * inB.x, inA.y * inB.y, inA.z * inB.z, inA.w * inB.w); }
constexpr uint4 operator / (const uint4 &inA, const uint4 &inB) { return uint4(inA.x / inB.x, inA.y / inB.y, inA.z / inB.z, inA.w / inB.w); }
constexpr uint4 operator * (const uint4 &inA, uint32 inS) { return uint4(inA.x * inS, inA.y * inS, inA.z * inS, inA.w * inS); }
constexpr uint4 operator * (uint32 inS, const uint4 &inA) { return inA * inS; }
constexpr uint4 operator / (const uint4 &inA, uint32 inS) { return uint4(inA.x / inS, inA.y / inS, inA.z / inS, inA.w / inS); }
// Dot product
constexpr uint32 dot(const uint4 &inA, const uint4 &inB) { return inA.x * inB.x + inA.y * inB.y + inA.z * inB.z + inA.w * inB.w; }
// Min value
constexpr uint4 min(const uint4 &inA, const uint4 &inB) { return uint4(min(inA.x, inB.x), min(inA.y, inB.y), min(inA.z, inB.z), min(inA.w, inB.w)); }
// Max value
constexpr uint4 max(const uint4 &inA, const uint4 &inB) { return uint4(max(inA.x, inB.x), max(inA.y, inB.y), max(inA.z, inB.z), max(inA.w, inB.w)); }
//////////////////////////////////////////////////////////////////////////////////////////
// int3
//////////////////////////////////////////////////////////////////////////////////////////
struct int3
{
inline int3() = default;
constexpr int3(int inX, int inY, int inZ) : x(inX), y(inY), z(inZ) { }
explicit constexpr int3(const float3 &inV) : x(int(inV.x)), y(int(inV.y)), z(int(inV.z)) { }
// Operators
constexpr int3 & operator += (const int3 &inRHS) { x += inRHS.x; y += inRHS.y; z += inRHS.z; return *this; }
constexpr int3 & operator -= (const int3 &inRHS) { x -= inRHS.x; y -= inRHS.y; z -= inRHS.z; return *this; }
constexpr int3 & operator *= (int inRHS) { x *= inRHS; y *= inRHS; z *= inRHS; return *this; }
constexpr int3 & operator /= (int inRHS) { x /= inRHS; y /= inRHS; z /= inRHS; return *this; }
constexpr int3 & operator *= (const int3 &inRHS) { x *= inRHS.x; y *= inRHS.y; z *= inRHS.z; return *this; }
constexpr int3 & operator /= (const int3 &inRHS) { x /= inRHS.x; y /= inRHS.y; z /= inRHS.z; return *this; }
// Equality
constexpr bool operator == (const int3 &inRHS) const { return x == inRHS.x && y == inRHS.y && z == inRHS.z; }
constexpr bool operator != (const int3 &inRHS) const { return !(*this == inRHS); }
// Component access
const int & operator [] (uint inIndex) const { return (&x)[inIndex]; }
int & operator [] (uint inIndex) { return (&x)[inIndex]; }
// Swizzling (note return value is const to prevent assignment to swizzled results)
const int3 swizzle_xyz() const { return int3(x, y, z); }
const int3 swizzle_xzy() const { return int3(x, z, y); }
const int3 swizzle_yxz() const { return int3(y, x, z); }
const int3 swizzle_yzx() const { return int3(y, z, x); }
const int3 swizzle_zxy() const { return int3(z, x, y); }
const int3 swizzle_zyx() const { return int3(z, y, x); }
int x, y, z;
};
// Operators
constexpr int3 operator - (const int3 &inA) { return int3(-inA.x, -inA.y, -inA.z); }
constexpr int3 operator + (const int3 &inA, const int3 &inB) { return int3(inA.x + inB.x, inA.y + inB.y, inA.z + inB.z); }
constexpr int3 operator - (const int3 &inA, const int3 &inB) { return int3(inA.x - inB.x, inA.y - inB.y, inA.z - inB.z); }
constexpr int3 operator * (const int3 &inA, const int3 &inB) { return int3(inA.x * inB.x, inA.y * inB.y, inA.z * inB.z); }
constexpr int3 operator / (const int3 &inA, const int3 &inB) { return int3(inA.x / inB.x, inA.y / inB.y, inA.z / inB.z); }
constexpr int3 operator * (const int3 &inA, int inS) { return int3(inA.x * inS, inA.y * inS, inA.z * inS); }
constexpr int3 operator * (int inS, const int3 &inA) { return inA * inS; }
constexpr int3 operator / (const int3 &inA, int inS) { return int3(inA.x / inS, inA.y / inS, inA.z / inS); }
// Dot product
constexpr int dot(const int3 &inA, const int3 &inB) { return inA.x * inB.x + inA.y * inB.y + inA.z * inB.z; }
// Min value
constexpr int3 min(const int3 &inA, const int3 &inB) { return int3(min(inA.x, inB.x), min(inA.y, inB.y), min(inA.z, inB.z)); }
// Max value
constexpr int3 max(const int3 &inA, const int3 &inB) { return int3(max(inA.x, inB.x), max(inA.y, inB.y), max(inA.z, inB.z)); }
//////////////////////////////////////////////////////////////////////////////////////////
// int4
//////////////////////////////////////////////////////////////////////////////////////////
struct int4
{
// Constructors
inline int4() = default;
constexpr int4(const int3 &inV, int inW) : x(inV.x), y(inV.y), z(inV.z), w(inW) { }
constexpr int4(int inX, int inY, int inZ, int inW) : x(inX), y(inY), z(inZ), w(inW) { }
explicit constexpr int4(int inS) : x(inS), y(inS), z(inS), w(inS) { }
explicit constexpr int4(const float4 &inV) : x(int(inV.x)), y(int(inV.y)), z(int(inV.z)), w(int(inV.w)) { }
// Operators
constexpr int4 & operator += (const int4 &inRHS) { x += inRHS.x; y += inRHS.y; z += inRHS.z; w += inRHS.w; return *this; }
constexpr int4 & operator -= (const int4 &inRHS) { x -= inRHS.x; y -= inRHS.y; z -= inRHS.z; w -= inRHS.w; return *this; }
constexpr int4 & operator *= (int inRHS) { x *= inRHS; y *= inRHS; z *= inRHS; w *= inRHS; return *this; }
constexpr int4 & operator /= (int inRHS) { x /= inRHS; y /= inRHS; z /= inRHS; w /= inRHS; return *this; }
constexpr int4 & operator *= (const int4 &inRHS) { x *= inRHS.x; y *= inRHS.y; z *= inRHS.z; w *= inRHS.w; return *this; }
constexpr int4 & operator /= (const int4 &inRHS) { x /= inRHS.x; y /= inRHS.y; z /= inRHS.z; w /= inRHS.w; return *this; }
// Equality
constexpr bool operator == (const int4 &inRHS) const { return x == inRHS.x && y == inRHS.y && z == inRHS.z && w == inRHS.w; }
constexpr bool operator != (const int4 &inRHS) const { return !(*this == inRHS); }
// Component access
const int & operator [] (uint inIndex) const { return (&x)[inIndex]; }
int & operator [] (uint inIndex) { return (&x)[inIndex]; }
// Swizzling (note return value is const to prevent assignment to swizzled results)
const int3 swizzle_xyz() const { return int3(x, y, z); }
const int3 swizzle_xzy() const { return int3(x, z, y); }
const int3 swizzle_yxz() const { return int3(y, x, z); }
const int3 swizzle_yzx() const { return int3(y, z, x); }
const int3 swizzle_zxy() const { return int3(z, x, y); }
const int3 swizzle_zyx() const { return int3(z, y, x); }
const int4 swizzle_xywz() const { return int4(x, y, w, z); }
const int4 swizzle_xwyz() const { return int4(x, w, y, z); }
const int4 swizzle_wxyz() const { return int4(w, x, y, z); }
int x, y, z, w;
};
// Operators
constexpr int4 operator - (const int4 &inA) { return int4(-inA.x, -inA.y, -inA.z, -inA.w); }
constexpr int4 operator + (const int4 &inA, const int4 &inB) { return int4(inA.x + inB.x, inA.y + inB.y, inA.z + inB.z, inA.w + inB.w); }
constexpr int4 operator - (const int4 &inA, const int4 &inB) { return int4(inA.x - inB.x, inA.y - inB.y, inA.z - inB.z, inA.w - inB.w); }
constexpr int4 operator * (const int4 &inA, const int4 &inB) { return int4(inA.x * inB.x, inA.y * inB.y, inA.z * inB.z, inA.w * inB.w); }
constexpr int4 operator / (const int4 &inA, const int4 &inB) { return int4(inA.x / inB.x, inA.y / inB.y, inA.z / inB.z, inA.w / inB.w); }
constexpr int4 operator * (const int4 &inA, int inS) { return int4(inA.x * inS, inA.y * inS, inA.z * inS, inA.w * inS); }
constexpr int4 operator * (int inS, const int4 &inA) { return inA * inS; }
constexpr int4 operator / (const int4 &inA, int inS) { return int4(inA.x / inS, inA.y / inS, inA.z / inS, inA.w / inS); }
// Dot product
constexpr int dot(const int4 &inA, const int4 &inB) { return inA.x * inB.x + inA.y * inB.y + inA.z * inB.z + inA.w * inB.w; }
// Min value
constexpr int4 min(const int4 &inA, const int4 &inB) { return int4(min(inA.x, inB.x), min(inA.y, inB.y), min(inA.z, inB.z), min(inA.w, inB.w)); }
// Max value
constexpr int4 max(const int4 &inA, const int4 &inB) { return int4(max(inA.x, inB.x), max(inA.y, inB.y), max(inA.z, inB.z), max(inA.w, inB.w)); }
//////////////////////////////////////////////////////////////////////////////////////////
// Mat44
//////////////////////////////////////////////////////////////////////////////////////////
struct Mat44
{
// Constructors
inline Mat44() = default;
constexpr Mat44(const float4 &inC0, const float4 &inC1, const float4 &inC2, const float4 &inC3) : c { inC0, inC1, inC2, inC3 } { }
// Columns
float4 & operator [] (uint inIndex) { return c[inIndex]; }
const float4 & operator [] (uint inIndex) const { return c[inIndex]; }
private:
float4 c[4];
};
//////////////////////////////////////////////////////////////////////////////////////////
// Other types
//////////////////////////////////////////////////////////////////////////////////////////
using Quat = float4;
using Plane = float4;
// Clamp value
template <class T>
constexpr T clamp(const T &inValue, const T &inMinValue, const T &inMaxValue)
{
return min(max(inValue, inMinValue), inMaxValue);
}
// Atomic add
template <class T>
T JPH_AtomicAdd(T &ioT, const T &inValue)
{
std::atomic<T> *value = reinterpret_cast<std::atomic<T> *>(&ioT);
return value->fetch_add(inValue) + inValue;
}
// Bitcast float4 to int4
inline int4 asint(const float4 &inV) { return int4(BitCast<int>(inV.x), BitCast<int>(inV.y), BitCast<int>(inV.z), BitCast<int>(inV.w)); }
// Functions that couldn't be declared earlier
constexpr float3::float3(const uint3 &inV) : x(float(inV.x)), y(float(inV.y)), z(float(inV.z)) { }
constexpr float4::float4(const int4 &inV) : x(float(inV.x)), y(float(inV.y)), z(float(inV.z)), w(float(inV.w)) { }
// Swizzle operators
#define xy swizzle_xy()
#define yx swizzle_yx()
#define xyz swizzle_xyz()
#define xzy swizzle_xzy()
#define yxz swizzle_yxz()
#define yzx swizzle_yzx()
#define zxy swizzle_zxy()
#define zyx swizzle_zyx()
#define xywz swizzle_xywz()
#define xwyz swizzle_xwyz()
#define wxyz swizzle_wxyz()
} // HLSLToCPP
JPH_NAMESPACE_END

View file

@ -0,0 +1,29 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2026 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#ifdef JPH_USE_CPU_COMPUTE
JPH_NAMESPACE_BEGIN
namespace HLSLToCPP { struct uint3; }
/// Wraps a compute shader to allow calling it from C++
class ShaderWrapper
{
public:
/// Destructor
virtual ~ShaderWrapper() = default;
/// Bind buffer to shader
virtual void Bind(const char *inName, void *inData, uint64 inSize) = 0;
/// Execute a single shader thread
virtual void Main(const HLSLToCPP::uint3 &inThreadID) = 0;
};
JPH_NAMESPACE_END
#endif // JPH_USE_CPU_COMPUTE

View file

@ -0,0 +1,75 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2026 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <Jolt/Core/HashCombine.h>
#include <Jolt/Compute/CPU/ComputeSystemCPU.h>
#include <Jolt/Compute/CPU/ShaderWrapper.h>
#include <Jolt/Compute/CPU/HLSLToCPP.h>
/// @cond INTERNAL
JPH_NAMESPACE_BEGIN
JPH_MSVC_SUPPRESS_WARNING(5031) // #pragma warning(pop): likely mismatch, popping warning state pushed in different file
#define JPH_SHADER_OVERRIDE_MACROS
#define JPH_SHADER_GENERATE_WRAPPER
#define JPH_SHADER_CONSTANT(type, name, value) inline static constexpr type name = value;
#define JPH_SHADER_CONSTANTS_BEGIN(type, name) struct type { alignas(16) int dummy; } name; // Ensure that the first constant is 16 byte aligned
#define JPH_SHADER_CONSTANTS_MEMBER(type, name) type c##name;
#define JPH_SHADER_CONSTANTS_END(type)
#define JPH_SHADER_BUFFER(type) const type *
#define JPH_SHADER_RW_BUFFER(type) type *
#define JPH_SHADER_BIND_BEGIN(name)
#define JPH_SHADER_BIND_END(name)
#define JPH_SHADER_BIND_BUFFER(type, name) const type *name = nullptr;
#define JPH_SHADER_BIND_RW_BUFFER(type, name) type *name = nullptr;
#define JPH_SHADER_FUNCTION_BEGIN(return_type, name, group_size_x, group_size_y, group_size_z) \
virtual void Main(
#define JPH_SHADER_PARAM_THREAD_ID(name) const HLSLToCPP::uint3 &name
#define JPH_SHADER_FUNCTION_END ) override
#define JPH_SHADER_STRUCT_BEGIN(name) struct name {
#define JPH_SHADER_STRUCT_MEMBER(type, name) type m##name;
#define JPH_SHADER_STRUCT_END(name) };
#define JPH_TO_STRING(name) JPH_TO_STRING2(name)
#define JPH_TO_STRING2(name) #name
#define JPH_SHADER_CLASS_NAME(name) JPH_SHADER_CLASS_NAME2(name)
#define JPH_SHADER_CLASS_NAME2(name) name##ShaderWrapper
#define JPH_IN(type) const type &
#define JPH_OUT(type) type &
#define JPH_IN_OUT(type) type &
// Namespace to prevent 'using' from leaking out
namespace ShaderWrappers {
using namespace HLSLToCPP;
class JPH_SHADER_CLASS_NAME(JPH_SHADER_NAME) : public ShaderWrapper
{
public:
// Define types
using JPH_float = float;
using JPH_float3 = HLSLToCPP::float3;
using JPH_float4 = HLSLToCPP::float4;
using JPH_uint = uint;
using JPH_uint3 = HLSLToCPP::uint3;
using JPH_uint4 = HLSLToCPP::uint4;
using JPH_int = int;
using JPH_int3 = HLSLToCPP::int3;
using JPH_int4 = HLSLToCPP::int4;
using JPH_Quat = HLSLToCPP::Quat;
using JPH_Plane = HLSLToCPP::Plane;
using JPH_Mat44 = HLSLToCPP::Mat44;
// Now the shader code should be included followed by WrapShaderBindings.h
/// @endcond

View file

@ -0,0 +1,40 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2026 Jorrit Rouwe
// SPDX-License-Identifier: MIT
/// @cond INTERNAL
// First WrapShaderBegin.h should have been included, then the shader code
/// Bind a buffer to the shader
virtual void Bind(const char *inName, void *inData, uint64 inSize) override
{
// Don't redefine constants
#undef JPH_SHADER_CONSTANT
#define JPH_SHADER_CONSTANT(type, name, value)
// Don't redefine structs
#undef JPH_SHADER_STRUCT_BEGIN
#undef JPH_SHADER_STRUCT_MEMBER
#undef JPH_SHADER_STRUCT_END
#define JPH_SHADER_STRUCT_BEGIN(name)
#define JPH_SHADER_STRUCT_MEMBER(type, name)
#define JPH_SHADER_STRUCT_END(name)
// When a constant buffer is bound, copy the data into the members
#undef JPH_SHADER_CONSTANTS_BEGIN
#undef JPH_SHADER_CONSTANTS_MEMBER
#define JPH_SHADER_CONSTANTS_BEGIN(type, name) case HashString(#name): memcpy(&name + 1, inData, size_t(inSize)); break; // Very hacky way to get the address of the first constant and to copy the entire block of constants
#define JPH_SHADER_CONSTANTS_MEMBER(type, name)
// When a buffer is bound, set the pointer
#undef JPH_SHADER_BIND_BUFFER
#undef JPH_SHADER_BIND_RW_BUFFER
#define JPH_SHADER_BIND_BUFFER(type, name) case HashString(#name): name = (const type *)inData; break;
#define JPH_SHADER_BIND_RW_BUFFER(type, name) case HashString(#name): name = (type *)inData; break;
switch (HashString(inName))
{
// Now include the shader bindings followed by WrapShaderEnd.h
/// @endcond

View file

@ -0,0 +1,61 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2026 Jorrit Rouwe
// SPDX-License-Identifier: MIT
/// @cond INTERNAL
// WrapShaderBindings.h should have been included followed by the shader bindings
default:
JPH_ASSERT(false, "Buffer cannot be bound to this shader");
break;
}
}
/// Factory function to create a shader wrapper for this shader
static ShaderWrapper * sCreate()
{
return new JPH_SHADER_CLASS_NAME(JPH_SHADER_NAME)();
}
};
} // ShaderWrappers
/// @endcond
// Stop clang from complaining that the register function is missing a prototype
JPH_SHADER_WRAPPER_FUNCTION(, JPH_SHADER_NAME);
/// Register this wrapper
JPH_SHADER_WRAPPER_FUNCTION(inComputeSystem, JPH_SHADER_NAME)
{
inComputeSystem->RegisterShader(JPH_TO_STRING(JPH_SHADER_NAME), ShaderWrappers::JPH_SHADER_CLASS_NAME(JPH_SHADER_NAME)::sCreate);
}
#undef JPH_SHADER_OVERRIDE_MACROS
#undef JPH_SHADER_GENERATE_WRAPPER
#undef JPH_SHADER_CONSTANT
#undef JPH_SHADER_CONSTANTS_BEGIN
#undef JPH_SHADER_CONSTANTS_MEMBER
#undef JPH_SHADER_CONSTANTS_END
#undef JPH_SHADER_BUFFER
#undef JPH_SHADER_RW_BUFFER
#undef JPH_SHADER_BIND_BEGIN
#undef JPH_SHADER_BIND_END
#undef JPH_SHADER_BIND_BUFFER
#undef JPH_SHADER_BIND_RW_BUFFER
#undef JPH_SHADER_FUNCTION_BEGIN
#undef JPH_SHADER_PARAM_THREAD_ID
#undef JPH_SHADER_FUNCTION_END
#undef JPH_SHADER_STRUCT_BEGIN
#undef JPH_SHADER_STRUCT_MEMBER
#undef JPH_SHADER_STRUCT_END
#undef JPH_TO_STRING
#undef JPH_TO_STRING2
#undef JPH_SHADER_CLASS_NAME
#undef JPH_SHADER_CLASS_NAME2
#undef JPH_OUT
#undef JPH_IN_OUT
#undef JPH_SHADER_NAME
JPH_NAMESPACE_END

View file

@ -0,0 +1,69 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/Core/Reference.h>
#include <Jolt/Core/NonCopyable.h>
#include <Jolt/Core/Result.h>
JPH_NAMESPACE_BEGIN
class ComputeBuffer;
using ComputeBufferResult = Result<Ref<ComputeBuffer>>;
/// Buffer that can be read from / written to by a compute shader
class JPH_EXPORT ComputeBuffer : public RefTarget<ComputeBuffer>, public NonCopyable
{
public:
JPH_OVERRIDE_NEW_DELETE
/// Type of buffer
enum class EType
{
UploadBuffer, ///< Buffer that can be written on the CPU and then uploaded to the GPU.
ReadbackBuffer, ///< Buffer to be sent from the GPU to the CPU, used to read back data.
ConstantBuffer, ///< A smallish buffer that is used to pass constants to a shader.
Buffer, ///< Buffer that can be read from by a shader. Must be initialized with data at construction time and is read only thereafter.
RWBuffer, ///< Buffer that can be read from and written to by a shader.
};
/// Constructor / Destructor
ComputeBuffer(EType inType, uint64 inSize, uint inStride) : mType(inType), mSize(inSize), mStride(inStride) { }
virtual ~ComputeBuffer() { JPH_ASSERT(!mIsMapped); }
/// Properties
EType GetType() const { return mType; }
uint64 GetSize() const { return mSize; }
uint GetStride() const { return mStride; }
/// Mode in which the buffer is accessed
enum class EMode
{
Read, ///< Read only access to the buffer
Write, ///< Write only access to the buffer (this will discard all previous data in the buffer)
};
/// Map / unmap buffer (get pointer to data).
void * Map(EMode inMode) { JPH_ASSERT(!mIsMapped); JPH_IF_ENABLE_ASSERTS(mIsMapped = true;) return MapInternal(inMode); }
template <typename T> T * Map(EMode inMode) { JPH_ASSERT(!mIsMapped); JPH_IF_ENABLE_ASSERTS(mIsMapped = true;) JPH_ASSERT(sizeof(T) == mStride); return reinterpret_cast<T *>(MapInternal(inMode)); }
void Unmap() { JPH_ASSERT(mIsMapped); JPH_IF_ENABLE_ASSERTS(mIsMapped = false;) UnmapInternal(); }
/// Create a readback buffer of the same size and stride that can be used to read the data stored in this buffer on CPU.
/// Note that this could also be implemented as 'return this' in case the underlying implementation allows locking GPU data on CPU directly.
virtual ComputeBufferResult CreateReadBackBuffer() const = 0;
protected:
EType mType;
uint64 mSize;
uint mStride;
#ifdef JPH_ENABLE_ASSERTS
bool mIsMapped = false;
#endif // JPH_ENABLE_ASSERTS
virtual void * MapInternal(EMode inMode) = 0;
virtual void UnmapInternal() = 0;
};
JPH_NAMESPACE_END

View file

@ -0,0 +1,83 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/Core/Reference.h>
#include <Jolt/Core/NonCopyable.h>
#include <Jolt/Core/Result.h>
JPH_NAMESPACE_BEGIN
class ComputeShader;
class ComputeBuffer;
/// A command queue for executing compute workloads on the GPU.
///
/// Note that only a single thread should be using a ComputeQueue at any time (although an implementation could be made that is thread safe).
class JPH_EXPORT ComputeQueue : public RefTarget<ComputeQueue>, public NonCopyable
{
public:
JPH_OVERRIDE_NEW_DELETE
/// Destructor
virtual ~ComputeQueue() = default;
/// Activate a shader. Shader must be set first before buffers can be bound.
/// After every Dispatch call, the shader must be set again and all buffers must be bound again.
virtual void SetShader(const ComputeShader *inShader) = 0;
/// If a barrier should be placed before accessing the buffer
enum class EBarrier
{
Yes,
No
};
/// Bind a constant buffer to the shader. Note that the contents of the buffer cannot be modified until execution finishes.
/// A reference to the buffer is added to make sure it stays alive until execution finishes.
/// @param inName Name of the buffer as specified in the shader.
/// @param inBuffer The buffer to bind.
virtual void SetConstantBuffer(const char *inName, const ComputeBuffer *inBuffer) = 0;
/// Bind a read only buffer to the shader. Note that the contents of the buffer cannot be modified on CPU until execution finishes (only relevant for buffers of type UploadBuffer).
/// A reference to the buffer is added to make sure it stays alive until execution finishes.
/// @param inName Name of the buffer as specified in the shader.
/// @param inBuffer The buffer to bind.
virtual void SetBuffer(const char *inName, const ComputeBuffer *inBuffer) = 0;
/// Bind a read/write buffer to the shader.
/// A reference to the buffer is added to make sure it stays alive until execution finishes.
/// @param inName Name of the buffer as specified in the shader.
/// @param inBuffer The buffer to bind.
/// @param inBarrier If set to Yes, a barrier will be placed before accessing the buffer to ensure all previous writes to the buffer are visible.
virtual void SetRWBuffer(const char *inName, ComputeBuffer *inBuffer, EBarrier inBarrier = EBarrier::Yes) = 0;
/// Dispatch a compute shader with the specified number of thread groups
virtual void Dispatch(uint inThreadGroupsX, uint inThreadGroupsY = 1, uint inThreadGroupsZ = 1) = 0;
/// Schedule buffer to be copied from GPU to CPU.
/// A reference to the buffers is added to make sure they stay alive until execution finishes.
virtual void ScheduleReadback(ComputeBuffer *inDst, const ComputeBuffer *inSrc) = 0;
/// Execute accumulated command list.
/// No more commands can be added until Wait is called.
virtual void Execute() = 0;
/// After executing, this waits until execution is done.
/// This also makes sure that any readback operations have completed and the data is available on CPU.
virtual void Wait() = 0;
/// Execute and wait for the command list to finish
/// @see Execute, Wait
void ExecuteAndWait()
{
Execute();
Wait();
}
};
using ComputeQueueResult = Result<Ref<ComputeQueue>>;
JPH_NAMESPACE_END

View file

@ -0,0 +1,41 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/Core/Reference.h>
#include <Jolt/Core/NonCopyable.h>
#include <Jolt/Core/Result.h>
JPH_NAMESPACE_BEGIN
/// Compute shader handle
class JPH_EXPORT ComputeShader : public RefTarget<ComputeShader>, public NonCopyable
{
public:
JPH_OVERRIDE_NEW_DELETE
/// Constructor / destructor
ComputeShader(uint32 inGroupSizeX, uint32 inGroupSizeY, uint32 inGroupSizeZ) :
mGroupSizeX(inGroupSizeX),
mGroupSizeY(inGroupSizeY),
mGroupSizeZ(inGroupSizeZ)
{
}
virtual ~ComputeShader() = default;
/// Get group sizes
uint32 GetGroupSizeX() const { return mGroupSizeX; }
uint32 GetGroupSizeY() const { return mGroupSizeY; }
uint32 GetGroupSizeZ() const { return mGroupSizeZ; }
private:
uint32 mGroupSizeX;
uint32 mGroupSizeY;
uint32 mGroupSizeZ;
};
using ComputeShaderResult = Result<Ref<ComputeShader>>;
JPH_NAMESPACE_END

View file

@ -0,0 +1,15 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2026 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <Jolt/Jolt.h>
#include <Jolt/Compute/ComputeSystem.h>
JPH_NAMESPACE_BEGIN
JPH_IMPLEMENT_RTTI_ABSTRACT_BASE(ComputeSystem)
{
}
JPH_NAMESPACE_END

View file

@ -0,0 +1,78 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/Compute/ComputeShader.h>
#include <Jolt/Compute/ComputeBuffer.h>
#include <Jolt/Compute/ComputeQueue.h>
#include <Jolt/Core/RTTI.h>
JPH_NAMESPACE_BEGIN
/// Interface to run a workload on the GPU
class JPH_EXPORT ComputeSystem : public RefTarget<ComputeSystem>, public NonCopyable
{
public:
JPH_DECLARE_RTTI_ABSTRACT_BASE(JPH_EXPORT, ComputeSystem)
/// Destructor
virtual ~ComputeSystem() = default;
/// Compile a compute shader
virtual ComputeShaderResult CreateComputeShader(const char *inName, uint32 inGroupSizeX, uint32 inGroupSizeY = 1, uint32 inGroupSizeZ = 1) = 0;
/// Create a buffer for use with a compute shader
virtual ComputeBufferResult CreateComputeBuffer(ComputeBuffer::EType inType, uint64 inSize, uint inStride, const void *inData = nullptr) = 0;
/// Create a queue for executing compute shaders
virtual ComputeQueueResult CreateComputeQueue() = 0;
/// Callback used when loading shaders
using ShaderLoader = std::function<bool(const char *inName, Array<uint8> &outData, String &outError)>;
ShaderLoader mShaderLoader = [](const char *, Array<uint8> &, String &outError) { JPH_ASSERT(false, "Override this function"); outError = "Not implemented"; return false; };
};
using ComputeSystemResult = Result<Ref<ComputeSystem>>;
#ifdef JPH_USE_VK
/// Factory function to create a compute system using Vulkan
extern JPH_EXPORT ComputeSystemResult CreateComputeSystemVK();
#endif
#ifdef JPH_USE_CPU_COMPUTE
/// Factory function to create a compute system that falls back to CPU.
/// This is intended mainly for debugging purposes and is not optimized for performance
extern JPH_EXPORT ComputeSystemResult CreateComputeSystemCPU();
#endif
#ifdef JPH_USE_DX12
/// Factory function to create a compute system using DirectX 12
extern JPH_EXPORT ComputeSystemResult CreateComputeSystemDX12();
/// Factory function to create the default compute system for this platform
inline ComputeSystemResult CreateComputeSystem() { return CreateComputeSystemDX12(); }
#elif defined(JPH_USE_MTL)
/// Factory function to create a compute system using Metal
extern JPH_EXPORT ComputeSystemResult CreateComputeSystemMTL();
/// Factory function to create the default compute system for this platform
inline ComputeSystemResult CreateComputeSystem() { return CreateComputeSystemMTL(); }
#elif defined(JPH_USE_VK)
/// Factory function to create the default compute system for this platform
inline ComputeSystemResult CreateComputeSystem() { return CreateComputeSystemVK(); }
#else
/// Fallback implementation when no compute system is available
inline ComputeSystemResult CreateComputeSystem() { ComputeSystemResult result; result.SetError("Not implemented"); return result; }
#endif
JPH_NAMESPACE_END

View file

@ -0,0 +1,167 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <Jolt/Jolt.h>
#ifdef JPH_USE_DX12
#include <Jolt/Compute/DX12/ComputeBufferDX12.h>
#include <Jolt/Compute/DX12/ComputeSystemDX12.h>
JPH_NAMESPACE_BEGIN
ComputeBufferDX12::ComputeBufferDX12(ComputeSystemDX12 *inComputeSystem, EType inType, uint64 inSize, uint inStride) :
ComputeBuffer(inType, inSize, inStride),
mComputeSystem(inComputeSystem)
{
}
bool ComputeBufferDX12::Initialize(const void *inData)
{
uint64 buffer_size = mSize * mStride;
switch (mType)
{
case EType::UploadBuffer:
mBufferCPU = mComputeSystem->CreateD3DResource(D3D12_HEAP_TYPE_UPLOAD, D3D12_RESOURCE_STATE_GENERIC_READ, D3D12_RESOURCE_FLAG_NONE, buffer_size);
mBufferGPU = mComputeSystem->CreateD3DResource(D3D12_HEAP_TYPE_DEFAULT, D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_FLAG_NONE, buffer_size);
if (mBufferCPU == nullptr || mBufferGPU == nullptr)
return false;
break;
case EType::ConstantBuffer:
mBufferCPU = mComputeSystem->CreateD3DResource(D3D12_HEAP_TYPE_UPLOAD, D3D12_RESOURCE_STATE_GENERIC_READ, D3D12_RESOURCE_FLAG_NONE, buffer_size);
if (mBufferCPU == nullptr)
return false;
break;
case EType::ReadbackBuffer:
JPH_ASSERT(inData == nullptr, "Can't upload data to a readback buffer");
mBufferCPU = mComputeSystem->CreateD3DResource(D3D12_HEAP_TYPE_READBACK, D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_FLAG_NONE, buffer_size);
if (mBufferCPU == nullptr)
return false;
break;
case EType::Buffer:
JPH_ASSERT(inData != nullptr);
mBufferCPU = mComputeSystem->CreateD3DResource(D3D12_HEAP_TYPE_UPLOAD, D3D12_RESOURCE_STATE_GENERIC_READ, D3D12_RESOURCE_FLAG_NONE, buffer_size);
mBufferGPU = mComputeSystem->CreateD3DResource(D3D12_HEAP_TYPE_DEFAULT, D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_FLAG_NONE, buffer_size);
if (mBufferCPU == nullptr || mBufferGPU == nullptr)
return false;
mNeedsSync = true;
break;
case EType::RWBuffer:
if (inData != nullptr)
{
mBufferCPU = mComputeSystem->CreateD3DResource(D3D12_HEAP_TYPE_UPLOAD, D3D12_RESOURCE_STATE_GENERIC_READ, D3D12_RESOURCE_FLAG_NONE, buffer_size);
if (mBufferCPU == nullptr)
return false;
mNeedsSync = true;
}
mBufferGPU = mComputeSystem->CreateD3DResource(D3D12_HEAP_TYPE_DEFAULT, D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS, buffer_size);
if (mBufferGPU == nullptr)
return false;
break;
}
// Copy data to upload buffer
if (inData != nullptr)
{
void *data = nullptr;
D3D12_RANGE range = { 0, 0 }; // We're not going to read
mBufferCPU->Map(0, &range, &data);
memcpy(data, inData, size_t(buffer_size));
mBufferCPU->Unmap(0, nullptr);
}
return true;
}
bool ComputeBufferDX12::Barrier(ID3D12GraphicsCommandList *inCommandList, D3D12_RESOURCE_STATES inTo) const
{
// Check if state changed
if (mCurrentState == inTo)
return false;
// Only buffers in GPU memory can change state
if (mType != ComputeBuffer::EType::Buffer && mType != ComputeBuffer::EType::RWBuffer)
return true;
D3D12_RESOURCE_BARRIER barrier;
barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE;
barrier.Transition.pResource = GetResourceGPU();
barrier.Transition.StateBefore = mCurrentState;
barrier.Transition.StateAfter = inTo;
barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
inCommandList->ResourceBarrier(1, &barrier);
mCurrentState = inTo;
return true;
}
void ComputeBufferDX12::RWBarrier(ID3D12GraphicsCommandList *inCommandList)
{
JPH_ASSERT(mCurrentState == D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
D3D12_RESOURCE_BARRIER barrier;
barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_UAV;
barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE;
barrier.Transition.pResource = GetResourceGPU();
inCommandList->ResourceBarrier(1, &barrier);
}
bool ComputeBufferDX12::SyncCPUToGPU(ID3D12GraphicsCommandList *inCommandList) const
{
if (!mNeedsSync)
return false;
Barrier(inCommandList, D3D12_RESOURCE_STATE_COPY_DEST);
inCommandList->CopyResource(GetResourceGPU(), GetResourceCPU());
mNeedsSync = false;
return true;
}
void *ComputeBufferDX12::MapInternal(EMode inMode)
{
void *mapped_resource = nullptr;
switch (inMode)
{
case EMode::Read:
JPH_ASSERT(mType == EType::ReadbackBuffer);
if (HRFailed(mBufferCPU->Map(0, nullptr, &mapped_resource)))
return nullptr;
break;
case EMode::Write:
{
JPH_ASSERT(mType == EType::UploadBuffer || mType == EType::ConstantBuffer);
D3D12_RANGE range = { 0, 0 }; // We're not going to read
if (HRFailed(mBufferCPU->Map(0, &range, &mapped_resource)))
return nullptr;
mNeedsSync = true;
}
break;
}
return mapped_resource;
}
void ComputeBufferDX12::UnmapInternal()
{
mBufferCPU->Unmap(0, nullptr);
}
ComputeBufferResult ComputeBufferDX12::CreateReadBackBuffer() const
{
return mComputeSystem->CreateComputeBuffer(EType::ReadbackBuffer, mSize, mStride);
}
JPH_NAMESPACE_END
#endif // JPH_USE_DX12

View file

@ -0,0 +1,51 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/Compute/ComputeBuffer.h>
#ifdef JPH_USE_DX12
#include <Jolt/Compute/DX12/IncludeDX12.h>
JPH_NAMESPACE_BEGIN
class ComputeSystemDX12;
/// Buffer that can be read from / written to by a compute shader
class JPH_EXPORT ComputeBufferDX12 final : public ComputeBuffer
{
public:
JPH_OVERRIDE_NEW_DELETE
/// Constructor
ComputeBufferDX12(ComputeSystemDX12 *inComputeSystem, EType inType, uint64 inSize, uint inStride);
bool Initialize(const void *inData);
ID3D12Resource * GetResourceCPU() const { return mBufferCPU.Get(); }
ID3D12Resource * GetResourceGPU() const { return mBufferGPU.Get(); }
ComPtr<ID3D12Resource> ReleaseResourceCPU() const { return std::move(mBufferCPU); }
bool Barrier(ID3D12GraphicsCommandList *inCommandList, D3D12_RESOURCE_STATES inTo) const;
void RWBarrier(ID3D12GraphicsCommandList *inCommandList);
bool SyncCPUToGPU(ID3D12GraphicsCommandList *inCommandList) const;
ComputeBufferResult CreateReadBackBuffer() const override;
private:
virtual void * MapInternal(EMode inMode) override;
virtual void UnmapInternal() override;
ComputeSystemDX12 * mComputeSystem;
mutable ComPtr<ID3D12Resource> mBufferCPU;
ComPtr<ID3D12Resource> mBufferGPU;
mutable bool mNeedsSync = false; ///< If this buffer needs to be synced from CPU to GPU
mutable D3D12_RESOURCE_STATES mCurrentState = D3D12_RESOURCE_STATE_COPY_DEST; ///< State of the GPU buffer so we can do proper barriers
};
JPH_NAMESPACE_END
#endif // JPH_USE_DX12

View file

@ -0,0 +1,221 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <Jolt/Jolt.h>
#ifdef JPH_USE_DX12
#include <Jolt/Compute/DX12/ComputeQueueDX12.h>
#include <Jolt/Compute/DX12/ComputeShaderDX12.h>
#include <Jolt/Compute/DX12/ComputeBufferDX12.h>
JPH_NAMESPACE_BEGIN
ComputeQueueDX12::~ComputeQueueDX12()
{
Wait();
if (mFenceEvent != INVALID_HANDLE_VALUE)
CloseHandle(mFenceEvent);
}
bool ComputeQueueDX12::Initialize(ID3D12Device *inDevice, D3D12_COMMAND_LIST_TYPE inType, ComputeQueueResult &outResult)
{
D3D12_COMMAND_QUEUE_DESC queue_desc = {};
queue_desc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
queue_desc.Type = inType;
queue_desc.Priority = D3D12_COMMAND_QUEUE_PRIORITY_HIGH;
if (HRFailed(inDevice->CreateCommandQueue(&queue_desc, IID_PPV_ARGS(&mCommandQueue)), outResult))
return false;
if (HRFailed(inDevice->CreateCommandAllocator(inType, IID_PPV_ARGS(&mCommandAllocator)), outResult))
return false;
// Create the command list
if (HRFailed(inDevice->CreateCommandList(0, inType, mCommandAllocator.Get(), nullptr, IID_PPV_ARGS(&mCommandList)), outResult))
return false;
// Command lists are created in the recording state, but there is nothing to record yet. The main loop expects it to be closed, so close it now
if (HRFailed(mCommandList->Close(), outResult))
return false;
// Create synchronization object
if (HRFailed(inDevice->CreateFence(mFenceValue, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&mFence)), outResult))
return false;
// Increment fence value so we don't skip waiting the first time a command list is executed
mFenceValue++;
// Create an event handle to use for frame synchronization
mFenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
if (HRFailed(HRESULT_FROM_WIN32(GetLastError()), outResult))
return false;
return true;
}
ID3D12GraphicsCommandList *ComputeQueueDX12::Start()
{
JPH_ASSERT(!mIsExecuting);
if (!mIsStarted)
{
// Reset the allocator
if (HRFailed(mCommandAllocator->Reset()))
return nullptr;
// Reset the command list
if (HRFailed(mCommandList->Reset(mCommandAllocator.Get(), nullptr)))
return nullptr;
// Now we have started recording commands
mIsStarted = true;
}
return mCommandList.Get();
}
void ComputeQueueDX12::SetShader(const ComputeShader *inShader)
{
ID3D12GraphicsCommandList *command_list = Start();
mShader = static_cast<const ComputeShaderDX12 *>(inShader);
command_list->SetPipelineState(mShader->GetPipelineState());
command_list->SetComputeRootSignature(mShader->GetRootSignature());
}
void ComputeQueueDX12::SyncCPUToGPU(const ComputeBufferDX12 *inBuffer)
{
// Ensure that any CPU writes are visible to the GPU
if (inBuffer->SyncCPUToGPU(mCommandList.Get())
&& (inBuffer->GetType() == ComputeBuffer::EType::Buffer || inBuffer->GetType() == ComputeBuffer::EType::RWBuffer))
{
// After the first upload, the CPU buffer is no longer needed for Buffer and RWBuffer types
mDelayedFreedBuffers.emplace_back(inBuffer->ReleaseResourceCPU());
}
}
void ComputeQueueDX12::SetConstantBuffer(const char *inName, const ComputeBuffer *inBuffer)
{
if (inBuffer == nullptr)
return;
JPH_ASSERT(inBuffer->GetType() == ComputeBuffer::EType::ConstantBuffer);
ID3D12GraphicsCommandList *command_list = Start();
const ComputeBufferDX12 *buffer = static_cast<const ComputeBufferDX12 *>(inBuffer);
command_list->SetComputeRootConstantBufferView(mShader->NameToIndex(inName), buffer->GetResourceCPU()->GetGPUVirtualAddress());
mUsedBuffers.insert(buffer);
}
void ComputeQueueDX12::SetBuffer(const char *inName, const ComputeBuffer *inBuffer)
{
if (inBuffer == nullptr)
return;
JPH_ASSERT(inBuffer->GetType() == ComputeBuffer::EType::UploadBuffer || inBuffer->GetType() == ComputeBuffer::EType::Buffer || inBuffer->GetType() == ComputeBuffer::EType::RWBuffer);
ID3D12GraphicsCommandList *command_list = Start();
const ComputeBufferDX12 *buffer = static_cast<const ComputeBufferDX12 *>(inBuffer);
uint parameter_index = mShader->NameToIndex(inName);
SyncCPUToGPU(buffer);
buffer->Barrier(command_list, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE);
command_list->SetComputeRootShaderResourceView(parameter_index, buffer->GetResourceGPU()->GetGPUVirtualAddress());
mUsedBuffers.insert(buffer);
}
void ComputeQueueDX12::SetRWBuffer(const char *inName, ComputeBuffer *inBuffer, EBarrier inBarrier)
{
if (inBuffer == nullptr)
return;
JPH_ASSERT(inBuffer->GetType() == ComputeBuffer::EType::RWBuffer);
ID3D12GraphicsCommandList *command_list = Start();
ComputeBufferDX12 *buffer = static_cast<ComputeBufferDX12 *>(inBuffer);
uint parameter_index = mShader->NameToIndex(inName);
SyncCPUToGPU(buffer);
if (!buffer->Barrier(command_list, D3D12_RESOURCE_STATE_UNORDERED_ACCESS) && inBarrier == EBarrier::Yes)
buffer->RWBarrier(command_list);
command_list->SetComputeRootUnorderedAccessView(parameter_index, buffer->GetResourceGPU()->GetGPUVirtualAddress());
mUsedBuffers.insert(buffer);
}
void ComputeQueueDX12::ScheduleReadback(ComputeBuffer *inDst, const ComputeBuffer *inSrc)
{
if (inDst == nullptr || inSrc == nullptr)
return;
JPH_ASSERT(inDst->GetType() == ComputeBuffer::EType::ReadbackBuffer);
ID3D12GraphicsCommandList *command_list = Start();
ComputeBufferDX12 *dst = static_cast<ComputeBufferDX12 *>(inDst);
const ComputeBufferDX12 *src = static_cast<const ComputeBufferDX12 *>(inSrc);
dst->Barrier(command_list, D3D12_RESOURCE_STATE_COPY_DEST);
src->Barrier(command_list, D3D12_RESOURCE_STATE_COPY_SOURCE);
command_list->CopyResource(dst->GetResourceCPU(), src->GetResourceGPU());
mUsedBuffers.insert(src);
mUsedBuffers.insert(dst);
}
void ComputeQueueDX12::Dispatch(uint inThreadGroupsX, uint inThreadGroupsY, uint inThreadGroupsZ)
{
ID3D12GraphicsCommandList *command_list = Start();
command_list->Dispatch(inThreadGroupsX, inThreadGroupsY, inThreadGroupsZ);
}
void ComputeQueueDX12::Execute()
{
JPH_ASSERT(mIsStarted);
JPH_ASSERT(!mIsExecuting);
// Close the command list
if (HRFailed(mCommandList->Close()))
return;
// Execute the command list
ID3D12CommandList *command_lists[] = { mCommandList.Get() };
mCommandQueue->ExecuteCommandLists((UINT)std::size(command_lists), command_lists);
// Schedule a Signal command in the queue
if (HRFailed(mCommandQueue->Signal(mFence.Get(), mFenceValue)))
return;
// Clear the current shader
mShader = nullptr;
// Mark that we're executing
mIsExecuting = true;
}
void ComputeQueueDX12::Wait()
{
// Check if we've been started
if (mIsExecuting)
{
if (mFence->GetCompletedValue() < mFenceValue)
{
// Wait until the fence has been processed
if (HRFailed(mFence->SetEventOnCompletion(mFenceValue, mFenceEvent)))
return;
WaitForSingleObjectEx(mFenceEvent, INFINITE, FALSE);
}
// Increment the fence value
mFenceValue++;
// Buffers can be freed now
mUsedBuffers.clear();
// Free buffers
mDelayedFreedBuffers.clear();
// Done executing
mIsExecuting = false;
mIsStarted = false;
}
}
JPH_NAMESPACE_END
#endif // JPH_USE_DX12

View file

@ -0,0 +1,61 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#ifdef JPH_USE_DX12
#include <Jolt/Compute/ComputeQueue.h>
#include <Jolt/Compute/DX12/ComputeShaderDX12.h>
#include <Jolt/Core/UnorderedSet.h>
JPH_NAMESPACE_BEGIN
class ComputeBufferDX12;
/// A command queue for DirectX for executing compute workloads on the GPU.
class JPH_EXPORT ComputeQueueDX12 final : public ComputeQueue
{
public:
JPH_OVERRIDE_NEW_DELETE
/// Destructor
virtual ~ComputeQueueDX12() override;
/// Initialize the queue
bool Initialize(ID3D12Device *inDevice, D3D12_COMMAND_LIST_TYPE inType, ComputeQueueResult &outResult);
/// Start the command list (requires waiting until the previous one is finished)
ID3D12GraphicsCommandList * Start();
// See: ComputeQueue
virtual void SetShader(const ComputeShader *inShader) override;
virtual void SetConstantBuffer(const char *inName, const ComputeBuffer *inBuffer) override;
virtual void SetBuffer(const char *inName, const ComputeBuffer *inBuffer) override;
virtual void SetRWBuffer(const char *inName, ComputeBuffer *inBuffer, EBarrier inBarrier = EBarrier::Yes) override;
virtual void ScheduleReadback(ComputeBuffer *inDst, const ComputeBuffer *inSrc) override;
virtual void Dispatch(uint inThreadGroupsX, uint inThreadGroupsY, uint inThreadGroupsZ) override;
virtual void Execute() override;
virtual void Wait() override;
private:
/// Copy the CPU buffer to the GPU buffer if needed
void SyncCPUToGPU(const ComputeBufferDX12 *inBuffer);
ComPtr<ID3D12CommandQueue> mCommandQueue; ///< The command queue that will hold command lists
ComPtr<ID3D12CommandAllocator> mCommandAllocator; ///< Allocator that holds the memory for the commands
ComPtr<ID3D12GraphicsCommandList> mCommandList; ///< The command list that will hold the render commands / state changes
HANDLE mFenceEvent = INVALID_HANDLE_VALUE; ///< Fence event, used to wait for rendering to complete
ComPtr<ID3D12Fence> mFence; ///< Fence object, used to signal the fence event
UINT64 mFenceValue = 0; ///< Current fence value, each time we need to wait we will signal the fence with this value, wait for it and then increase the value
RefConst<ComputeShaderDX12> mShader = nullptr; ///< Current active shader
bool mIsStarted = false; ///< If the command list has been started (reset) and is ready to record commands
bool mIsExecuting = false; ///< If a command list is currently executing on the queue
UnorderedSet<RefConst<ComputeBuffer>> mUsedBuffers; ///< Buffers that are in use by the current execution, these will be retained until execution is finished so that we don't free buffers that are in use
Array<ComPtr<ID3D12Resource>> mDelayedFreedBuffers; ///< Buffers freed during the execution
};
JPH_NAMESPACE_END
#endif // JPH_USE_DX12

View file

@ -0,0 +1,52 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#ifdef JPH_USE_DX12
#include <Jolt/Compute/ComputeShader.h>
#include <Jolt/Compute/DX12/IncludeDX12.h>
#include <Jolt/Core/UnorderedMap.h>
JPH_NAMESPACE_BEGIN
/// Compute shader handle for DirectX
class JPH_EXPORT ComputeShaderDX12 : public ComputeShader
{
public:
JPH_OVERRIDE_NEW_DELETE
/// Constructor
ComputeShaderDX12(ComPtr<ID3D12RootSignature> inRootSignature, ComPtr<ID3D12PipelineState> inPipelineState, Array<String> &&inBindingNames, UnorderedMap<string_view, uint> &&inNameToIndex, uint32 inGroupSizeX, uint32 inGroupSizeY, uint32 inGroupSizeZ) :
ComputeShader(inGroupSizeX, inGroupSizeY, inGroupSizeZ),
mRootSignature(inRootSignature),
mPipelineState(inPipelineState),
mBindingNames(std::move(inBindingNames)),
mNameToIndex(std::move(inNameToIndex))
{
}
/// Get index of shader parameter
uint NameToIndex(const char *inName) const
{
UnorderedMap<string_view, uint>::const_iterator it = mNameToIndex.find(inName);
JPH_ASSERT(it != mNameToIndex.end());
return it->second;
}
/// Getters
ID3D12PipelineState * GetPipelineState() const { return mPipelineState.Get(); }
ID3D12RootSignature * GetRootSignature() const { return mRootSignature.Get(); }
private:
ComPtr<ID3D12RootSignature> mRootSignature; ///< The root signature for this shader
ComPtr<ID3D12PipelineState> mPipelineState; ///< The pipeline state object for this shader
Array<String> mBindingNames; ///< A list of binding names, mNameToIndex points to these strings
UnorderedMap<string_view, uint> mNameToIndex; ///< Maps names to indices for the shader parameters, using a string_view so we can do find() without an allocation
};
JPH_NAMESPACE_END
#endif // JPH_USE_DX12

View file

@ -0,0 +1,255 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <Jolt/Jolt.h>
#ifdef JPH_USE_DX12
#include <Jolt/Compute/DX12/ComputeSystemDX12.h>
#include <Jolt/Compute/DX12/ComputeQueueDX12.h>
#include <Jolt/Compute/DX12/ComputeShaderDX12.h>
#include <Jolt/Compute/DX12/ComputeBufferDX12.h>
#include <Jolt/Core/StringTools.h>
#include <Jolt/Core/UnorderedMap.h>
#include <Jolt/Core/FPException.h>
JPH_SUPPRESS_WARNINGS_STD_BEGIN
JPH_MSVC_SUPPRESS_WARNING(5204) // 'X': class has virtual functions, but its trivial destructor is not virtual; instances of objects derived from this class may not be destructed correctly
JPH_MSVC2026_PLUS_SUPPRESS_WARNING(4865) // wingdi.h(2806,1): '<unnamed-enum-DISPLAYCONFIG_OUTPUT_TECHNOLOGY_OTHER>': the underlying type will change from 'int' to '__int64' when '/Zc:enumTypes' is specified on the command line
#include <d3dcompiler.h>
#include <dxcapi.h>
#ifdef JPH_DEBUG
#include <d3d12sdklayers.h>
#endif
JPH_SUPPRESS_WARNINGS_STD_END
JPH_NAMESPACE_BEGIN
JPH_IMPLEMENT_RTTI_VIRTUAL(ComputeSystemDX12)
{
JPH_ADD_BASE_CLASS(ComputeSystemDX12, ComputeSystem)
}
bool ComputeSystemDX12::Initialize(ID3D12Device *inDevice, ComputeSystemResult &outResult)
{
mDevice = inDevice;
// Dynamically load dxcompiler.dll
HMODULE dxc_module = LoadLibraryA("dxcompiler.dll");
if (dxc_module == nullptr)
{
outResult.SetError("Failed to load dxcompiler.dll");
return false;
}
mDxcCreateInstanceFn = GetProcAddress(dxc_module, "DxcCreateInstance");
JPH_ASSERT(mDxcCreateInstanceFn != nullptr);
return true;
}
void ComputeSystemDX12::Shutdown()
{
// Releasing the device can cause invalid floating point operation exceptions inside the DirectX driver
FPExceptionDisableInvalid disable_invalid;
JPH_UNUSED(disable_invalid);
mDevice.Reset();
}
ComPtr<ID3D12Resource> ComputeSystemDX12::CreateD3DResource(D3D12_HEAP_TYPE inHeapType, D3D12_RESOURCE_STATES inResourceState, D3D12_RESOURCE_FLAGS inFlags, uint64 inSize)
{
// Create a new resource
D3D12_RESOURCE_DESC desc;
desc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
desc.Alignment = 0;
desc.Width = inSize;
desc.Height = 1;
desc.DepthOrArraySize = 1;
desc.MipLevels = 1;
desc.Format = DXGI_FORMAT_UNKNOWN;
desc.SampleDesc.Count = 1;
desc.SampleDesc.Quality = 0;
desc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
desc.Flags = inFlags;
D3D12_HEAP_PROPERTIES heap_properties = {};
heap_properties.Type = inHeapType;
heap_properties.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
heap_properties.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
heap_properties.CreationNodeMask = 1;
heap_properties.VisibleNodeMask = 1;
ComPtr<ID3D12Resource> resource;
if (HRFailed(mDevice->CreateCommittedResource(&heap_properties, D3D12_HEAP_FLAG_NONE, &desc, inResourceState, nullptr, IID_PPV_ARGS(&resource))))
return nullptr;
return resource;
}
ComputeShaderResult ComputeSystemDX12::CreateComputeShader(const char *inName, uint32 inGroupSizeX, uint32 inGroupSizeY, uint32 inGroupSizeZ)
{
ComputeShaderResult result;
// Read shader source file
Array<uint8> data;
String error;
String file_name = String(inName) + ".dxil";
if (!mShaderLoader(file_name.c_str(), data, error))
{
result.SetError(error);
return result;
}
// Create IDxcUtils object
ComPtr<IDxcUtils> utils;
reinterpret_cast<DxcCreateInstanceProc>(reinterpret_cast<void *>(mDxcCreateInstanceFn))(CLSID_DxcUtils, IID_PPV_ARGS(utils.GetAddressOf()));
// Get reflection data
DxcBuffer reflection_buffer = { data.data(), data.size(), 0 };
ComPtr<ID3D12ShaderReflection> reflector;
if (HRFailed(utils->CreateReflection(&reflection_buffer, IID_PPV_ARGS(reflector.GetAddressOf())), result))
return result;
// Get the shader description
D3D12_SHADER_DESC shader_desc;
if (HRFailed(reflector->GetDesc(&shader_desc), result))
return result;
// Verify that the group sizes match the shader's thread group size
UINT thread_group_size_x, thread_group_size_y, thread_group_size_z;
if (HRFailed(reflector->GetThreadGroupSize(&thread_group_size_x, &thread_group_size_y, &thread_group_size_z), result))
return result;
JPH_ASSERT(inGroupSizeX == thread_group_size_x, "Group size X mismatch");
JPH_ASSERT(inGroupSizeY == thread_group_size_y, "Group size Y mismatch");
JPH_ASSERT(inGroupSizeZ == thread_group_size_z, "Group size Z mismatch");
// Convert parameters to root signature description
Array<String> binding_names;
binding_names.reserve(shader_desc.BoundResources);
UnorderedMap<string_view, uint> name_to_index;
Array<D3D12_ROOT_PARAMETER1> root_params;
for (UINT i = 0; i < shader_desc.BoundResources; ++i)
{
D3D12_SHADER_INPUT_BIND_DESC bind_desc;
reflector->GetResourceBindingDesc(i, &bind_desc);
D3D12_ROOT_PARAMETER1 param = {};
param.ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL;
switch (bind_desc.Type)
{
case D3D_SIT_CBUFFER:
param.ParameterType = D3D12_ROOT_PARAMETER_TYPE_CBV;
break;
case D3D_SIT_STRUCTURED:
case D3D_SIT_BYTEADDRESS:
param.ParameterType = D3D12_ROOT_PARAMETER_TYPE_SRV;
break;
case D3D_SIT_UAV_RWTYPED:
case D3D_SIT_UAV_RWSTRUCTURED:
case D3D_SIT_UAV_RWBYTEADDRESS:
case D3D_SIT_UAV_APPEND_STRUCTURED:
case D3D_SIT_UAV_CONSUME_STRUCTURED:
case D3D_SIT_UAV_RWSTRUCTURED_WITH_COUNTER:
param.ParameterType = D3D12_ROOT_PARAMETER_TYPE_UAV;
break;
case D3D_SIT_TBUFFER:
case D3D_SIT_TEXTURE:
case D3D_SIT_SAMPLER:
case D3D_SIT_RTACCELERATIONSTRUCTURE:
case D3D_SIT_UAV_FEEDBACKTEXTURE:
JPH_ASSERT(false, "Unsupported shader input type");
continue;
}
param.Descriptor.RegisterSpace = bind_desc.Space;
param.Descriptor.ShaderRegister = bind_desc.BindPoint;
param.Descriptor.Flags = D3D12_ROOT_DESCRIPTOR_FLAG_DATA_VOLATILE;
binding_names.push_back(bind_desc.Name); // Add all strings to a pool to keep them alive
name_to_index[string_view(binding_names.back())] = (uint)root_params.size();
root_params.push_back(param);
}
// Create the root signature
D3D12_VERSIONED_ROOT_SIGNATURE_DESC root_sig_desc = {};
root_sig_desc.Version = D3D_ROOT_SIGNATURE_VERSION_1_1;
root_sig_desc.Desc_1_1.NumParameters = (UINT)root_params.size();
root_sig_desc.Desc_1_1.pParameters = root_params.data();
root_sig_desc.Desc_1_1.NumStaticSamplers = 0;
root_sig_desc.Desc_1_1.pStaticSamplers = nullptr;
root_sig_desc.Desc_1_1.Flags = D3D12_ROOT_SIGNATURE_FLAG_NONE;
ComPtr<ID3DBlob> serialized_sig;
ComPtr<ID3DBlob> root_sig_error_blob;
if (FAILED(D3D12SerializeVersionedRootSignature(&root_sig_desc, &serialized_sig, &root_sig_error_blob)))
{
if (root_sig_error_blob)
{
error = StringFormat("Failed to create root signature: %s", (const char *)root_sig_error_blob->GetBufferPointer());
result.SetError(error);
}
else
result.SetError("Failed to create root signature");
return result;
}
ComPtr<ID3D12RootSignature> root_sig;
if (FAILED(mDevice->CreateRootSignature(0, serialized_sig->GetBufferPointer(), serialized_sig->GetBufferSize(), IID_PPV_ARGS(&root_sig))))
{
result.SetError("Failed to create root signature");
return result;
}
// Create a pipeline state object from the root signature and the shader
ComPtr<ID3D12PipelineState> pipeline_state;
D3D12_COMPUTE_PIPELINE_STATE_DESC compute_state_desc = {};
compute_state_desc.pRootSignature = root_sig.Get();
compute_state_desc.CS = { data.data(), data.size() };
if (FAILED(mDevice->CreateComputePipelineState(&compute_state_desc, IID_PPV_ARGS(&pipeline_state))))
{
result.SetError("Failed to create compute pipeline state");
return result;
}
// Set name on DX12 objects for easier debugging
wchar_t w_name[1024];
size_t converted_chars = 0;
mbstowcs_s(&converted_chars, w_name, 1024, inName, _TRUNCATE);
pipeline_state->SetName(w_name);
result.Set(new ComputeShaderDX12(root_sig, pipeline_state, std::move(binding_names), std::move(name_to_index), inGroupSizeX, inGroupSizeY, inGroupSizeZ));
return result;
}
ComputeBufferResult ComputeSystemDX12::CreateComputeBuffer(ComputeBuffer::EType inType, uint64 inSize, uint inStride, const void *inData)
{
ComputeBufferResult result;
Ref<ComputeBufferDX12> buffer = new ComputeBufferDX12(this, inType, inSize, inStride);
if (!buffer->Initialize(inData))
{
result.SetError("Failed to create compute buffer");
return result;
}
result.Set(buffer.GetPtr());
return result;
}
ComputeQueueResult ComputeSystemDX12::CreateComputeQueue()
{
ComputeQueueResult result;
Ref<ComputeQueueDX12> queue = new ComputeQueueDX12();
if (!queue->Initialize(mDevice.Get(), D3D12_COMMAND_LIST_TYPE_COMPUTE, result))
return result;
result.Set(queue.GetPtr());
return result;
}
JPH_NAMESPACE_END
#endif // JPH_USE_DX12

View file

@ -0,0 +1,45 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/Core/UnorderedMap.h>
#include <Jolt/Compute/ComputeSystem.h>
#ifdef JPH_USE_DX12
#include <Jolt/Compute/DX12/IncludeDX12.h>
JPH_NAMESPACE_BEGIN
/// Interface to run a workload on the GPU using DirectX 12.
/// Minimal implementation that can integrate with your own DirectX 12 setup.
class JPH_EXPORT ComputeSystemDX12 : public ComputeSystem
{
public:
JPH_DECLARE_RTTI_VIRTUAL(JPH_EXPORT, ComputeSystemDX12)
/// Initialize / shutdown
bool Initialize(ID3D12Device *inDevice, ComputeSystemResult &outResult);
void Shutdown();
// See: ComputeSystem
virtual ComputeShaderResult CreateComputeShader(const char *inName, uint32 inGroupSizeX, uint32 inGroupSizeY, uint32 inGroupSizeZ) override;
virtual ComputeBufferResult CreateComputeBuffer(ComputeBuffer::EType inType, uint64 inSize, uint inStride, const void *inData = nullptr) override;
virtual ComputeQueueResult CreateComputeQueue() override;
/// Access to the DX12 device
ID3D12Device * GetDevice() const { return mDevice.Get(); }
// Function to create a ID3D12Resource on specified heap with specified state
ComPtr<ID3D12Resource> CreateD3DResource(D3D12_HEAP_TYPE inHeapType, D3D12_RESOURCE_STATES inResourceState, D3D12_RESOURCE_FLAGS inFlags, uint64 inSize);
private:
ComPtr<ID3D12Device> mDevice;
FARPROC mDxcCreateInstanceFn = nullptr;
};
JPH_NAMESPACE_END
#endif // JPH_USE_DX12

View file

@ -0,0 +1,158 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <Jolt/Jolt.h>
#ifdef JPH_USE_DX12
#include <Jolt/Compute/DX12/ComputeSystemDX12Impl.h>
#ifdef JPH_DEBUG
#include <d3d12sdklayers.h>
#endif
JPH_NAMESPACE_BEGIN
JPH_IMPLEMENT_RTTI_VIRTUAL(ComputeSystemDX12Impl)
{
JPH_ADD_BASE_CLASS(ComputeSystemDX12Impl, ComputeSystemDX12)
}
ComputeSystemDX12Impl::~ComputeSystemDX12Impl()
{
Shutdown();
mDXGIFactory.Reset();
#ifdef JPH_DEBUG
// Test for leaks
ComPtr<IDXGIDebug1> dxgi_debug;
if (SUCCEEDED(DXGIGetDebugInterface1(0, IID_PPV_ARGS(&dxgi_debug))))
dxgi_debug->ReportLiveObjects(DXGI_DEBUG_ALL, DXGI_DEBUG_RLO_ALL);
#endif
}
bool ComputeSystemDX12Impl::Initialize(ComputeSystemResult &outResult)
{
#if defined(JPH_DEBUG)
// Enable the D3D12 debug layer
ComPtr<ID3D12Debug> debug_controller;
if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debug_controller))))
debug_controller->EnableDebugLayer();
#endif
// Create DXGI factory
if (HRFailed(CreateDXGIFactory1(IID_PPV_ARGS(&mDXGIFactory)), outResult))
{
outResult.SetError("Failed to create DXGI factory");
return false;
}
// Find adapter
ComPtr<IDXGIAdapter1> adapter;
ComPtr<ID3D12Device> device;
HRESULT result = E_FAIL;
// First check if we have the Windows 1803 IDXGIFactory6 interface
ComPtr<IDXGIFactory6> factory6;
if (SUCCEEDED(mDXGIFactory->QueryInterface(IID_PPV_ARGS(&factory6))))
{
for (int search_software = 0; search_software < 2 && device == nullptr; ++search_software)
for (UINT index = 0; factory6->EnumAdapterByGpuPreference(index, DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE, IID_PPV_ARGS(&adapter)) != DXGI_ERROR_NOT_FOUND; ++index)
{
DXGI_ADAPTER_DESC1 desc;
adapter->GetDesc1(&desc);
// We don't want software renderers in the first pass
int is_software = (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) != 0? 1 : 0;
if (search_software != is_software)
continue;
// Check to see whether the adapter supports Direct3D 12
#if defined(JPH_PLATFORM_WINDOWS) && defined(_DEBUG)
int prev_state = _CrtSetDbgFlag(0); // Temporarily disable leak detection as this call reports false positives
#endif
result = D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device));
#if defined(JPH_PLATFORM_WINDOWS) && defined(_DEBUG)
_CrtSetDbgFlag(prev_state);
#endif
if (SUCCEEDED(result))
break;
}
}
else
{
// Fall back to the older method that may not get the fastest GPU
for (int search_software = 0; search_software < 2 && device == nullptr; ++search_software)
for (UINT index = 0; mDXGIFactory->EnumAdapters1(index, &adapter) != DXGI_ERROR_NOT_FOUND; ++index)
{
DXGI_ADAPTER_DESC1 desc;
adapter->GetDesc1(&desc);
// We don't want software renderers in the first pass
int is_software = (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) != 0? 1 : 0;
if (search_software != is_software)
continue;
// Check to see whether the adapter supports Direct3D 12
#if defined(JPH_PLATFORM_WINDOWS) && defined(_DEBUG)
int prev_state = _CrtSetDbgFlag(0); // Temporarily disable leak detection as this call reports false positives
#endif
result = D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device));
#if defined(JPH_PLATFORM_WINDOWS) && defined(_DEBUG)
_CrtSetDbgFlag(prev_state);
#endif
if (SUCCEEDED(result))
break;
}
}
// Check if we managed to obtain a device
if (HRFailed(result, outResult))
return false;
// Initialize the compute interface
if (!ComputeSystemDX12::Initialize(device.Get(), outResult))
return false;
#ifdef JPH_DEBUG
// Enable breaking on errors
ComPtr<ID3D12InfoQueue> info_queue;
if (SUCCEEDED(device.As(&info_queue)))
{
info_queue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_CORRUPTION, TRUE);
info_queue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_ERROR, TRUE);
info_queue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_WARNING, TRUE);
// Disable an error that triggers on Windows 11 with a hybrid graphic system
// See: https://stackoverflow.com/questions/69805245/directx-12-application-is-crashing-in-windows-11
D3D12_MESSAGE_ID hide[] =
{
D3D12_MESSAGE_ID_RESOURCE_BARRIER_MISMATCHING_COMMAND_LIST_TYPE,
};
D3D12_INFO_QUEUE_FILTER filter = { };
filter.DenyList.NumIDs = static_cast<UINT>(std::size(hide));
filter.DenyList.pIDList = hide;
info_queue->AddStorageFilterEntries(&filter);
}
#endif // JPH_DEBUG
return true;
}
ComputeSystemResult CreateComputeSystemDX12()
{
ComputeSystemResult result;
Ref<ComputeSystemDX12Impl> compute = new ComputeSystemDX12Impl();
if (!compute->Initialize(result))
return result;
result.Set(compute.GetPtr());
return result;
}
JPH_NAMESPACE_END
#endif // JPH_USE_DX12

View file

@ -0,0 +1,33 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#ifdef JPH_USE_DX12
#include <Jolt/Compute/DX12/ComputeSystemDX12.h>
JPH_NAMESPACE_BEGIN
/// Implementation of ComputeSystemDX12 that fully initializes DirectX 12
class JPH_EXPORT ComputeSystemDX12Impl : public ComputeSystemDX12
{
public:
JPH_DECLARE_RTTI_VIRTUAL(JPH_EXPORT, ComputeSystemDX12Impl)
/// Destructor
virtual ~ComputeSystemDX12Impl() override;
/// Initialize the compute system
bool Initialize(ComputeSystemResult &outResult);
IDXGIFactory4 * GetDXGIFactory() const { return mDXGIFactory.Get(); }
private:
ComPtr<IDXGIFactory4> mDXGIFactory;
};
JPH_NAMESPACE_END
#endif // JPH_USE_DX12

View file

@ -0,0 +1,49 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/Core/IncludeWindows.h>
#include <Jolt/Core/StringTools.h>
JPH_SUPPRESS_WARNINGS_STD_BEGIN
JPH_MSVC_SUPPRESS_WARNING(4265) // 'X': class has virtual functions, but its non-trivial destructor is not virtual; instances of this class may not be destructed correctly
JPH_MSVC_SUPPRESS_WARNING(4625) // 'X': copy constructor was implicitly defined as deleted
JPH_MSVC_SUPPRESS_WARNING(4626) // 'X': assignment operator was implicitly defined as deleted
JPH_MSVC_SUPPRESS_WARNING(5204) // 'X': class has virtual functions, but its trivial destructor is not virtual; instances of objects derived from this class may not be destructed correctly
JPH_MSVC_SUPPRESS_WARNING(5220) // 'X': a non-static data member with a volatile qualified type no longer implies
JPH_MSVC2026_PLUS_SUPPRESS_WARNING(4865) // wingdi.h(2806,1): '<unnamed-enum-DISPLAYCONFIG_OUTPUT_TECHNOLOGY_OTHER>': the underlying type will change from 'int' to '__int64' when '/Zc:enumTypes' is specified on the command line
#include <d3d12.h>
#include <dxgi1_6.h>
#include <dxgidebug.h>
#include <wrl.h>
JPH_SUPPRESS_WARNINGS_STD_END
JPH_NAMESPACE_BEGIN
using Microsoft::WRL::ComPtr;
template <class Result>
inline bool HRFailed(HRESULT inHR, Result &outResult)
{
if (SUCCEEDED(inHR))
return false;
String error = StringFormat("Call failed with error code: %08X", inHR);
outResult.SetError(error);
JPH_ASSERT(false);
return true;
}
inline bool HRFailed(HRESULT inHR)
{
if (SUCCEEDED(inHR))
return false;
Trace("Call failed with error code: %08X", inHR);
JPH_ASSERT(false);
return true;
}
JPH_NAMESPACE_END

View file

@ -0,0 +1,39 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#ifdef JPH_USE_MTL
#include <Jolt/Compute/MTL/ComputeSystemMTL.h>
JPH_NAMESPACE_BEGIN
/// Buffer that can be read from / written to by a compute shader
class JPH_EXPORT ComputeBufferMTL final : public ComputeBuffer
{
public:
JPH_OVERRIDE_NEW_DELETE
/// Constructor
ComputeBufferMTL(ComputeSystemMTL *inComputeSystem, EType inType, uint64 inSize, uint inStride);
virtual ~ComputeBufferMTL() override;
bool Initialize(const void *inData);
virtual ComputeBufferResult CreateReadBackBuffer() const override;
id<MTLBuffer> GetBuffer() const { return mBuffer; }
private:
virtual void * MapInternal(EMode inMode) override;
virtual void UnmapInternal() override;
ComputeSystemMTL * mComputeSystem;
id<MTLBuffer> mBuffer;
};
JPH_NAMESPACE_END
#endif // JPH_USE_MTL

View file

@ -0,0 +1,52 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <Jolt/Jolt.h>
#ifdef JPH_USE_MTL
#include <Jolt/Compute/MTL/ComputeBufferMTL.h>
JPH_NAMESPACE_BEGIN
ComputeBufferMTL::ComputeBufferMTL(ComputeSystemMTL *inComputeSystem, EType inType, uint64 inSize, uint inStride) :
ComputeBuffer(inType, inSize, inStride),
mComputeSystem(inComputeSystem)
{
}
bool ComputeBufferMTL::Initialize(const void *inData)
{
NSUInteger size = NSUInteger(mSize) * mStride;
if (inData != nullptr)
mBuffer = [mComputeSystem->GetDevice() newBufferWithBytes: inData length: size options: MTLResourceCPUCacheModeDefaultCache | MTLResourceStorageModeShared | MTLResourceHazardTrackingModeTracked];
else
mBuffer = [mComputeSystem->GetDevice() newBufferWithLength: size options: MTLResourceCPUCacheModeDefaultCache | MTLResourceStorageModeShared | MTLResourceHazardTrackingModeTracked];
return mBuffer != nil;
}
ComputeBufferMTL::~ComputeBufferMTL()
{
[mBuffer release];
}
void *ComputeBufferMTL::MapInternal(EMode inMode)
{
return mBuffer.contents;
}
void ComputeBufferMTL::UnmapInternal()
{
}
ComputeBufferResult ComputeBufferMTL::CreateReadBackBuffer() const
{
ComputeBufferResult result;
result.Set(const_cast<ComputeBufferMTL *>(this));
return result;
}
JPH_NAMESPACE_END
#endif // JPH_USE_MTL

View file

@ -0,0 +1,49 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#ifdef JPH_USE_MTL
#include <MetalKit/MetalKit.h>
#include <Jolt/Compute/ComputeQueue.h>
JPH_NAMESPACE_BEGIN
class ComputeShaderMTL;
/// A command queue for Metal for executing compute workloads on the GPU.
class JPH_EXPORT ComputeQueueMTL final : public ComputeQueue
{
public:
JPH_OVERRIDE_NEW_DELETE
/// Constructor / destructor
ComputeQueueMTL(id<MTLDevice> inDevice);
virtual ~ComputeQueueMTL() override;
// See: ComputeQueue
virtual void SetShader(const ComputeShader *inShader) override;
virtual void SetConstantBuffer(const char *inName, const ComputeBuffer *inBuffer) override;
virtual void SetBuffer(const char *inName, const ComputeBuffer *inBuffer) override;
virtual void SetRWBuffer(const char *inName, ComputeBuffer *inBuffer, EBarrier inBarrier = EBarrier::Yes) override;
virtual void ScheduleReadback(ComputeBuffer *inDst, const ComputeBuffer *inSrc) override;
virtual void Dispatch(uint inThreadGroupsX, uint inThreadGroupsY, uint inThreadGroupsZ) override;
virtual void Execute() override;
virtual void Wait() override;
private:
void BeginCommandBuffer();
id<MTLCommandQueue> mCommandQueue;
id<MTLCommandBuffer> mCommandBuffer;
id<MTLComputeCommandEncoder> mComputeEncoder;
RefConst<ComputeShaderMTL> mShader;
bool mIsExecuting = false;
};
JPH_NAMESPACE_END
#endif // JPH_USE_MTL

View file

@ -0,0 +1,123 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <Jolt/Jolt.h>
#ifdef JPH_USE_MTL
#include <Jolt/Compute/MTL/ComputeQueueMTL.h>
#include <Jolt/Compute/MTL/ComputeShaderMTL.h>
#include <Jolt/Compute/MTL/ComputeBufferMTL.h>
#include <Jolt/Compute/MTL/ComputeSystemMTL.h>
JPH_NAMESPACE_BEGIN
ComputeQueueMTL::~ComputeQueueMTL()
{
Wait();
[mCommandQueue release];
}
ComputeQueueMTL::ComputeQueueMTL(id<MTLDevice> inDevice)
{
// Create the command queue
mCommandQueue = [inDevice newCommandQueue];
}
void ComputeQueueMTL::BeginCommandBuffer()
{
if (mCommandBuffer == nil)
{
// Start a new command buffer
mCommandBuffer = [mCommandQueue commandBuffer];
mComputeEncoder = [mCommandBuffer computeCommandEncoder];
}
}
void ComputeQueueMTL::SetShader(const ComputeShader *inShader)
{
BeginCommandBuffer();
mShader = static_cast<const ComputeShaderMTL *>(inShader);
[mComputeEncoder setComputePipelineState: mShader->GetPipelineState()];
}
void ComputeQueueMTL::SetConstantBuffer(const char *inName, const ComputeBuffer *inBuffer)
{
if (inBuffer == nullptr)
return;
JPH_ASSERT(inBuffer->GetType() == ComputeBuffer::EType::ConstantBuffer);
BeginCommandBuffer();
const ComputeBufferMTL *buffer = static_cast<const ComputeBufferMTL *>(inBuffer);
[mComputeEncoder setBuffer: buffer->GetBuffer() offset: 0 atIndex: mShader->NameToBindingIndex(inName)];
}
void ComputeQueueMTL::SetBuffer(const char *inName, const ComputeBuffer *inBuffer)
{
if (inBuffer == nullptr)
return;
JPH_ASSERT(inBuffer->GetType() == ComputeBuffer::EType::UploadBuffer || inBuffer->GetType() == ComputeBuffer::EType::Buffer || inBuffer->GetType() == ComputeBuffer::EType::RWBuffer);
BeginCommandBuffer();
const ComputeBufferMTL *buffer = static_cast<const ComputeBufferMTL *>(inBuffer);
[mComputeEncoder setBuffer: buffer->GetBuffer() offset: 0 atIndex: mShader->NameToBindingIndex(inName)];
}
void ComputeQueueMTL::SetRWBuffer(const char *inName, ComputeBuffer *inBuffer, EBarrier inBarrier)
{
if (inBuffer == nullptr)
return;
JPH_ASSERT(inBuffer->GetType() == ComputeBuffer::EType::RWBuffer);
BeginCommandBuffer();
const ComputeBufferMTL *buffer = static_cast<const ComputeBufferMTL *>(inBuffer);
[mComputeEncoder setBuffer: buffer->GetBuffer() offset: 0 atIndex: mShader->NameToBindingIndex(inName)];
}
void ComputeQueueMTL::ScheduleReadback(ComputeBuffer *inDst, const ComputeBuffer *inSrc)
{
JPH_ASSERT(inDst == inSrc); // Since ComputeBuffer::CreateReadBackBuffer returns the same buffer, we don't need to copy
}
void ComputeQueueMTL::Dispatch(uint inThreadGroupsX, uint inThreadGroupsY, uint inThreadGroupsZ)
{
BeginCommandBuffer();
MTLSize thread_groups = MTLSizeMake(inThreadGroupsX, inThreadGroupsY, inThreadGroupsZ);
MTLSize group_size = MTLSizeMake(mShader->GetGroupSizeX(), mShader->GetGroupSizeY(), mShader->GetGroupSizeZ());
[mComputeEncoder dispatchThreadgroups: thread_groups threadsPerThreadgroup: group_size];
}
void ComputeQueueMTL::Execute()
{
// End command buffer
if (mCommandBuffer == nil)
return;
[mComputeEncoder endEncoding];
[mCommandBuffer commit];
mShader = nullptr;
mIsExecuting = true;
}
void ComputeQueueMTL::Wait()
{
if (!mIsExecuting)
return;
[mCommandBuffer waitUntilCompleted];
mComputeEncoder = nil;
mCommandBuffer = nil;
mIsExecuting = false;
}
JPH_NAMESPACE_END
#endif // JPH_USE_MTL

View file

@ -0,0 +1,39 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#ifdef JPH_USE_MTL
#include <MetalKit/MetalKit.h>
#include <Jolt/Compute/ComputeShader.h>
#include <Jolt/Core/UnorderedMap.h>
JPH_NAMESPACE_BEGIN
/// Compute shader handle for Metal
class JPH_EXPORT ComputeShaderMTL : public ComputeShader
{
public:
JPH_OVERRIDE_NEW_DELETE
/// Constructor
ComputeShaderMTL(id<MTLComputePipelineState> inPipelineState, MTLComputePipelineReflection *inReflection, uint32 inGroupSizeX, uint32 inGroupSizeY, uint32 inGroupSizeZ);
virtual ~ComputeShaderMTL() override { [mPipelineState release]; }
/// Access to the function
id<MTLComputePipelineState> GetPipelineState() const { return mPipelineState; }
/// Get index of buffer name
uint NameToBindingIndex(const char *inName) const;
private:
id<MTLComputePipelineState> mPipelineState;
UnorderedMap<String, uint> mNameToBindingIndex;
};
JPH_NAMESPACE_END
#endif // JPH_USE_MTL

View file

@ -0,0 +1,34 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <Jolt/Jolt.h>
#ifdef JPH_USE_MTL
#include <Jolt/Compute/MTL/ComputeShaderMTL.h>
JPH_NAMESPACE_BEGIN
ComputeShaderMTL::ComputeShaderMTL(id<MTLComputePipelineState> inPipelineState, MTLComputePipelineReflection *inReflection, uint32 inGroupSizeX, uint32 inGroupSizeY, uint32 inGroupSizeZ) :
ComputeShader(inGroupSizeX, inGroupSizeY, inGroupSizeZ),
mPipelineState(inPipelineState)
{
for (id<MTLBinding> binding in inReflection.bindings)
{
const char *name = [binding.name UTF8String];
uint index = uint(binding.index);
mNameToBindingIndex[name] = index;
}
}
uint ComputeShaderMTL::NameToBindingIndex(const char *inName) const
{
UnorderedMap<String, uint>::const_iterator it = mNameToBindingIndex.find(inName);
JPH_ASSERT(it != mNameToBindingIndex.end());
return it->second;
}
JPH_NAMESPACE_END
#endif // JPH_USE_MTL

View file

@ -0,0 +1,40 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/Compute/ComputeSystem.h>
#ifdef JPH_USE_MTL
#include <MetalKit/MetalKit.h>
JPH_NAMESPACE_BEGIN
/// Interface to run a workload on the GPU
class JPH_EXPORT ComputeSystemMTL : public ComputeSystem
{
public:
JPH_DECLARE_RTTI_VIRTUAL(JPH_EXPORT, ComputeSystemMTL)
// Initialize / shutdown the compute system
bool Initialize(id<MTLDevice> inDevice);
void Shutdown();
// See: ComputeSystem
virtual ComputeShaderResult CreateComputeShader(const char *inName, uint32 inGroupSizeX, uint32 inGroupSizeY, uint32 inGroupSizeZ) override;
virtual ComputeBufferResult CreateComputeBuffer(ComputeBuffer::EType inType, uint64 inSize, uint inStride, const void *inData = nullptr) override;
virtual ComputeQueueResult CreateComputeQueue() override;
/// Get the metal device
id<MTLDevice> GetDevice() const { return mDevice; }
private:
id<MTLDevice> mDevice;
id<MTLLibrary> mShaderLibrary;
};
JPH_NAMESPACE_END
#endif // JPH_USE_MTL

View file

@ -0,0 +1,110 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <Jolt/Jolt.h>
#ifdef JPH_USE_MTL
#include <Jolt/Compute/MTL/ComputeSystemMTL.h>
#include <Jolt/Compute/MTL/ComputeBufferMTL.h>
#include <Jolt/Compute/MTL/ComputeShaderMTL.h>
#include <Jolt/Compute/MTL/ComputeQueueMTL.h>
JPH_NAMESPACE_BEGIN
JPH_IMPLEMENT_RTTI_VIRTUAL(ComputeSystemMTL)
{
JPH_ADD_BASE_CLASS(ComputeSystemMTL, ComputeSystem)
}
bool ComputeSystemMTL::Initialize(id<MTLDevice> inDevice)
{
mDevice = [inDevice retain];
return true;
}
void ComputeSystemMTL::Shutdown()
{
[mShaderLibrary release];
[mDevice release];
}
ComputeShaderResult ComputeSystemMTL::CreateComputeShader(const char *inName, uint32 inGroupSizeX, uint32 inGroupSizeY, uint32 inGroupSizeZ)
{
ComputeShaderResult result;
if (mShaderLibrary == nil)
{
// Load the shader library containing all shaders
Array<uint8> *data = new Array<uint8>();
String error;
if (!mShaderLoader("Jolt.metallib", *data, error))
{
result.SetError(error);
delete data;
return result;
}
// Convert to dispatch data
dispatch_data_t data_dispatch = dispatch_data_create(data->data(), data->size(), nullptr, ^{ delete data; });
// Create the library
NSError *ns_error = nullptr;
mShaderLibrary = [mDevice newLibraryWithData: data_dispatch error: &ns_error];
if (ns_error != nil)
{
result.SetError("Failed to laod shader library");
return result;
}
}
// Get the shader function
id<MTLFunction> function = [mShaderLibrary newFunctionWithName: [NSString stringWithCString: inName encoding: NSUTF8StringEncoding]];
if (function == nil)
{
result.SetError("Failed to instantiate compute shader");
return result;
}
// Create the pipeline
NSError *error = nil;
MTLComputePipelineReflection *reflection = nil;
id<MTLComputePipelineState> pipeline_state = [mDevice newComputePipelineStateWithFunction: function options: MTLPipelineOptionBindingInfo | MTLPipelineOptionBufferTypeInfo reflection: &reflection error: &error];
if (error != nil || pipeline_state == nil)
{
result.SetError("Failed to create compute pipeline");
[function release];
return result;
}
result.Set(new ComputeShaderMTL(pipeline_state, reflection, inGroupSizeX, inGroupSizeY, inGroupSizeZ));
return result;
}
ComputeBufferResult ComputeSystemMTL::CreateComputeBuffer(ComputeBuffer::EType inType, uint64 inSize, uint inStride, const void *inData)
{
ComputeBufferResult result;
Ref<ComputeBufferMTL> buffer = new ComputeBufferMTL(this, inType, inSize, inStride);
if (!buffer->Initialize(inData))
{
result.SetError("Failed to create compute buffer");
return result;
}
result.Set(buffer.GetPtr());
return result;
}
ComputeQueueResult ComputeSystemMTL::CreateComputeQueue()
{
ComputeQueueResult result;
result.Set(new ComputeQueueMTL(mDevice));
return result;
}
JPH_NAMESPACE_END
#endif // JPH_USE_MTL

View file

@ -0,0 +1,28 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#ifdef JPH_USE_MTL
#include <Jolt/Compute/MTL/ComputeSystemMTL.h>
JPH_NAMESPACE_BEGIN
/// Interface to run a workload on the GPU that fully initializes Metal.
class JPH_EXPORT ComputeSystemMTLImpl : public ComputeSystemMTL
{
public:
JPH_DECLARE_RTTI_VIRTUAL(JPH_EXPORT, ComputeSystemMTLImpl)
/// Destructor
virtual ~ComputeSystemMTLImpl() override;
/// Initialize / shutdown the compute system
bool Initialize();
};
JPH_NAMESPACE_END
#endif // JPH_USE_MTL

View file

@ -0,0 +1,49 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <Jolt/Jolt.h>
#ifdef JPH_USE_MTL
#include <Jolt/Compute/MTL/ComputeSystemMTLImpl.h>
JPH_NAMESPACE_BEGIN
JPH_IMPLEMENT_RTTI_VIRTUAL(ComputeSystemMTLImpl)
{
JPH_ADD_BASE_CLASS(ComputeSystemMTLImpl, ComputeSystemMTL)
}
ComputeSystemMTLImpl::~ComputeSystemMTLImpl()
{
Shutdown();
[GetDevice() release];
}
bool ComputeSystemMTLImpl::Initialize()
{
id<MTLDevice> device = MTLCreateSystemDefaultDevice();
return ComputeSystemMTL::Initialize(device);
}
ComputeSystemResult CreateComputeSystemMTL()
{
ComputeSystemResult result;
Ref<ComputeSystemMTLImpl> compute = new ComputeSystemMTLImpl;
if (!compute->Initialize())
{
result.SetError("Failed to initialize compute system");
return result;
}
result.Set(compute.GetPtr());
return result;
}
JPH_NAMESPACE_END
#endif // JPH_USE_MTL

View file

@ -0,0 +1,42 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/Compute/VK/IncludeVK.h>
#include <Jolt/Core/Reference.h>
#include <Jolt/Core/NonCopyable.h>
JPH_NAMESPACE_BEGIN
/// Simple wrapper class to manage a Vulkan memory block
class MemoryVK : public RefTarget<MemoryVK>, public NonCopyable
{
public:
~MemoryVK()
{
// We should have unmapped and freed the block before destruction
JPH_ASSERT(mMappedCount == 0);
JPH_ASSERT(mMemory == VK_NULL_HANDLE);
}
VkDeviceMemory mMemory = VK_NULL_HANDLE; ///< The Vulkan memory handle
VkDeviceSize mSize = 0; ///< Size of the memory block
VkDeviceSize mBufferSize = 0; ///< Size of each of the buffers that this memory block has been divided into
VkMemoryPropertyFlags mProperties = 0; ///< Vulkan memory properties used to allocate this block
int mMappedCount = 0; ///< How often buffers using this memory block were mapped
void * mMappedPtr = nullptr; ///< The CPU address of the memory block when mapped
};
/// Simple wrapper class to manage a Vulkan buffer
class BufferVK
{
public:
Ref<MemoryVK> mMemory; ///< The memory block that contains the buffer (note that filling this in is optional if you do your own buffer allocation)
VkBuffer mBuffer = VK_NULL_HANDLE; ///< The Vulkan buffer handle
VkDeviceSize mOffset = 0; ///< Offset in the memory block where the buffer starts
VkDeviceSize mSize = 0; ///< Real size of the buffer
};
JPH_NAMESPACE_END

View file

@ -0,0 +1,140 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <Jolt/Jolt.h>
#ifdef JPH_USE_VK
#include <Jolt/Compute/VK/ComputeBufferVK.h>
#include <Jolt/Compute/VK/ComputeSystemVK.h>
JPH_NAMESPACE_BEGIN
ComputeBufferVK::ComputeBufferVK(ComputeSystemVK *inComputeSystem, EType inType, uint64 inSize, uint inStride) :
ComputeBuffer(inType, inSize, inStride),
mComputeSystem(inComputeSystem)
{
}
bool ComputeBufferVK::Initialize(const void *inData)
{
VkDeviceSize buffer_size = VkDeviceSize(mSize * mStride);
switch (mType)
{
case EType::Buffer:
JPH_ASSERT(inData != nullptr);
[[fallthrough]];
case EType::UploadBuffer:
case EType::RWBuffer:
if (!mComputeSystem->CreateBuffer(buffer_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT | VK_MEMORY_PROPERTY_HOST_CACHED_BIT, mBufferCPU))
return false;
if (!mComputeSystem->CreateBuffer(buffer_size, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, mBufferGPU))
return false;
if (inData != nullptr)
{
void *data = mComputeSystem->MapBuffer(mBufferCPU);
memcpy(data, inData, size_t(buffer_size));
mComputeSystem->UnmapBuffer(mBufferCPU);
mNeedsSync = true;
}
break;
case EType::ConstantBuffer:
if (!mComputeSystem->CreateBuffer(buffer_size, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT | VK_MEMORY_PROPERTY_HOST_CACHED_BIT, mBufferCPU))
return false;
if (inData != nullptr)
{
void* data = mComputeSystem->MapBuffer(mBufferCPU);
memcpy(data, inData, size_t(buffer_size));
mComputeSystem->UnmapBuffer(mBufferCPU);
}
break;
case EType::ReadbackBuffer:
JPH_ASSERT(inData == nullptr, "Can't upload data to a readback buffer");
if (!mComputeSystem->CreateBuffer(buffer_size, VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT | VK_MEMORY_PROPERTY_HOST_CACHED_BIT, mBufferCPU))
return false;
break;
}
return true;
}
ComputeBufferVK::~ComputeBufferVK()
{
mComputeSystem->FreeBuffer(mBufferGPU);
mComputeSystem->FreeBuffer(mBufferCPU);
}
void ComputeBufferVK::Barrier(VkCommandBuffer inCommandBuffer, VkPipelineStageFlags inToStage, VkAccessFlagBits inToFlags, bool inForce) const
{
if (mAccessStage == inToStage && mAccessFlagBits == inToFlags && !inForce)
return;
VkBufferMemoryBarrier b = {};
b.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER;
b.srcAccessMask = mAccessFlagBits;
b.dstAccessMask = inToFlags;
b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
b.buffer = mBufferGPU.mBuffer != VK_NULL_HANDLE? mBufferGPU.mBuffer : mBufferCPU.mBuffer;
b.offset = 0;
b.size = VK_WHOLE_SIZE;
mComputeSystem->mVkCmdPipelineBarrier(inCommandBuffer, mAccessStage, inToStage, 0, 0, nullptr, 1, &b, 0, nullptr);
mAccessStage = inToStage;
mAccessFlagBits = inToFlags;
}
bool ComputeBufferVK::SyncCPUToGPU(VkCommandBuffer inCommandBuffer) const
{
if (!mNeedsSync)
return false;
// Barrier before write
Barrier(inCommandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, false);
// Copy from CPU to GPU
VkBufferCopy copy = {};
copy.srcOffset = 0;
copy.dstOffset = 0;
copy.size = GetSize() * GetStride();
mComputeSystem->mVkCmdCopyBuffer(inCommandBuffer, mBufferCPU.mBuffer, mBufferGPU.mBuffer, 1, &copy);
mNeedsSync = false;
return true;
}
void *ComputeBufferVK::MapInternal(EMode inMode)
{
switch (inMode)
{
case EMode::Read:
JPH_ASSERT(mType == EType::ReadbackBuffer);
break;
case EMode::Write:
JPH_ASSERT(mType == EType::UploadBuffer || mType == EType::ConstantBuffer);
mNeedsSync = true;
break;
}
return mComputeSystem->MapBuffer(mBufferCPU);
}
void ComputeBufferVK::UnmapInternal()
{
mComputeSystem->UnmapBuffer(mBufferCPU);
}
ComputeBufferResult ComputeBufferVK::CreateReadBackBuffer() const
{
return mComputeSystem->CreateComputeBuffer(ComputeBuffer::EType::ReadbackBuffer, mSize, mStride);
}
JPH_NAMESPACE_END
#endif // JPH_USE_VK

View file

@ -0,0 +1,52 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/Compute/ComputeBuffer.h>
#ifdef JPH_USE_VK
#include <Jolt/Compute/VK/BufferVK.h>
JPH_NAMESPACE_BEGIN
class ComputeSystemVK;
/// Buffer that can be read from / written to by a compute shader
class JPH_EXPORT ComputeBufferVK final : public ComputeBuffer
{
public:
JPH_OVERRIDE_NEW_DELETE
/// Constructor
ComputeBufferVK(ComputeSystemVK *inComputeSystem, EType inType, uint64 inSize, uint inStride);
virtual ~ComputeBufferVK() override;
bool Initialize(const void *inData);
virtual ComputeBufferResult CreateReadBackBuffer() const override;
VkBuffer GetBufferCPU() const { return mBufferCPU.mBuffer; }
VkBuffer GetBufferGPU() const { return mBufferGPU.mBuffer; }
BufferVK ReleaseBufferCPU() const { BufferVK tmp = mBufferCPU; mBufferCPU = BufferVK(); return tmp; }
void Barrier(VkCommandBuffer inCommandBuffer, VkPipelineStageFlags inToStage, VkAccessFlagBits inToFlags, bool inForce) const;
bool SyncCPUToGPU(VkCommandBuffer inCommandBuffer) const;
private:
virtual void * MapInternal(EMode inMode) override;
virtual void UnmapInternal() override;
ComputeSystemVK * mComputeSystem;
mutable BufferVK mBufferCPU;
BufferVK mBufferGPU;
mutable bool mNeedsSync = false; ///< If this buffer needs to be synced from CPU to GPU
mutable VkAccessFlagBits mAccessFlagBits = VK_ACCESS_SHADER_READ_BIT; ///< Access flags of the last usage, used for barriers
mutable VkPipelineStageFlags mAccessStage = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT; ///< Pipeline stage of the last usage, used for barriers
};
JPH_NAMESPACE_END
#endif // JPH_USE_VK

View file

@ -0,0 +1,304 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <Jolt/Jolt.h>
#ifdef JPH_USE_VK
#include <Jolt/Compute/VK/ComputeQueueVK.h>
#include <Jolt/Compute/VK/ComputeBufferVK.h>
#include <Jolt/Compute/VK/ComputeSystemVK.h>
JPH_NAMESPACE_BEGIN
ComputeQueueVK::~ComputeQueueVK()
{
Wait();
VkDevice device = mComputeSystem->GetDevice();
if (mCommandBuffer != VK_NULL_HANDLE)
mComputeSystem->mVkFreeCommandBuffers(device, mCommandPool, 1, &mCommandBuffer);
if (mCommandPool != VK_NULL_HANDLE)
mComputeSystem->mVkDestroyCommandPool(device, mCommandPool, nullptr);
if (mDescriptorPool != VK_NULL_HANDLE)
mComputeSystem->mVkDestroyDescriptorPool(device, mDescriptorPool, nullptr);
if (mFence != VK_NULL_HANDLE)
mComputeSystem->mVkDestroyFence(device, mFence, nullptr);
}
bool ComputeQueueVK::Initialize(uint32 inComputeQueueIndex, ComputeQueueResult &outResult)
{
// Get the queue
VkDevice device = mComputeSystem->GetDevice();
mComputeSystem->mVkGetDeviceQueue(device, inComputeQueueIndex, 0, &mQueue);
// Create a command pool
VkCommandPoolCreateInfo pool_info = {};
pool_info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
pool_info.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
pool_info.queueFamilyIndex = inComputeQueueIndex;
if (VKFailed(mComputeSystem->mVkCreateCommandPool(device, &pool_info, nullptr, &mCommandPool), outResult))
return false;
// Create descriptor pool
VkDescriptorPoolSize descriptor_pool_sizes[] = {
{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1024 },
{ VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 16 * 1024 },
};
VkDescriptorPoolCreateInfo descriptor_info = {};
descriptor_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
descriptor_info.poolSizeCount = (uint32)std::size(descriptor_pool_sizes);
descriptor_info.pPoolSizes = descriptor_pool_sizes;
descriptor_info.maxSets = 256;
if (VKFailed(mComputeSystem->mVkCreateDescriptorPool(device, &descriptor_info, nullptr, &mDescriptorPool), outResult))
return false;
// Create a command buffer
VkCommandBufferAllocateInfo alloc_info = {};
alloc_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
alloc_info.commandPool = mCommandPool;
alloc_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
alloc_info.commandBufferCount = 1;
if (VKFailed(mComputeSystem->mVkAllocateCommandBuffers(device, &alloc_info, &mCommandBuffer), outResult))
return false;
// Create a fence
VkFenceCreateInfo fence_info = {};
fence_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
if (VKFailed(mComputeSystem->mVkCreateFence(device, &fence_info, nullptr, &mFence), outResult))
return false;
return true;
}
bool ComputeQueueVK::BeginCommandBuffer()
{
if (!mCommandBufferRecording)
{
VkCommandBufferBeginInfo begin_info = {};
begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
begin_info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
if (VKFailed(mComputeSystem->mVkBeginCommandBuffer(mCommandBuffer, &begin_info)))
return false;
mCommandBufferRecording = true;
}
return true;
}
void ComputeQueueVK::SetShader(const ComputeShader *inShader)
{
mShader = static_cast<const ComputeShaderVK *>(inShader);
mBufferInfos = mShader->GetBufferInfos();
}
void ComputeQueueVK::SetConstantBuffer(const char *inName, const ComputeBuffer *inBuffer)
{
if (inBuffer == nullptr)
return;
JPH_ASSERT(inBuffer->GetType() == ComputeBuffer::EType::ConstantBuffer);
if (!BeginCommandBuffer())
return;
const ComputeBufferVK *buffer = static_cast<const ComputeBufferVK *>(inBuffer);
buffer->Barrier(mCommandBuffer, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_UNIFORM_READ_BIT, false);
uint index = mShader->NameToBufferInfoIndex(inName);
JPH_ASSERT(mShader->GetLayoutBindings()[index].descriptorType == VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER);
mBufferInfos[index].buffer = buffer->GetBufferCPU();
mUsedBuffers.insert(buffer);
}
void ComputeQueueVK::SyncCPUToGPU(const ComputeBufferVK *inBuffer)
{
// Ensure that any CPU writes are visible to the GPU
if (inBuffer->SyncCPUToGPU(mCommandBuffer)
&& (inBuffer->GetType() == ComputeBuffer::EType::Buffer || inBuffer->GetType() == ComputeBuffer::EType::RWBuffer))
{
// After the first upload, the CPU buffer is no longer needed for Buffer and RWBuffer types
mDelayedFreedBuffers.push_back(inBuffer->ReleaseBufferCPU());
}
}
void ComputeQueueVK::SetBuffer(const char *inName, const ComputeBuffer *inBuffer)
{
if (inBuffer == nullptr)
return;
JPH_ASSERT(inBuffer->GetType() == ComputeBuffer::EType::UploadBuffer || inBuffer->GetType() == ComputeBuffer::EType::Buffer || inBuffer->GetType() == ComputeBuffer::EType::RWBuffer);
if (!BeginCommandBuffer())
return;
const ComputeBufferVK *buffer = static_cast<const ComputeBufferVK *>(inBuffer);
SyncCPUToGPU(buffer);
buffer->Barrier(mCommandBuffer, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT, false);
uint index = mShader->NameToBufferInfoIndex(inName);
JPH_ASSERT(mShader->GetLayoutBindings()[index].descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER);
mBufferInfos[index].buffer = buffer->GetBufferGPU();
mUsedBuffers.insert(buffer);
}
void ComputeQueueVK::SetRWBuffer(const char *inName, ComputeBuffer *inBuffer, EBarrier inBarrier)
{
if (inBuffer == nullptr)
return;
JPH_ASSERT(inBuffer->GetType() == ComputeBuffer::EType::RWBuffer);
if (!BeginCommandBuffer())
return;
const ComputeBufferVK *buffer = static_cast<const ComputeBufferVK *>(inBuffer);
SyncCPUToGPU(buffer);
buffer->Barrier(mCommandBuffer, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VkAccessFlagBits(VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT), inBarrier == EBarrier::Yes);
uint index = mShader->NameToBufferInfoIndex(inName);
JPH_ASSERT(mShader->GetLayoutBindings()[index].descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER);
mBufferInfos[index].buffer = buffer->GetBufferGPU();
mUsedBuffers.insert(buffer);
}
void ComputeQueueVK::ScheduleReadback(ComputeBuffer *inDst, const ComputeBuffer *inSrc)
{
if (inDst == nullptr || inSrc == nullptr)
return;
JPH_ASSERT(inDst->GetType() == ComputeBuffer::EType::ReadbackBuffer);
if (!BeginCommandBuffer())
return;
const ComputeBufferVK *src_vk = static_cast<const ComputeBufferVK *>(inSrc);
const ComputeBufferVK *dst_vk = static_cast<ComputeBufferVK *>(inDst);
// Barrier to start reading from GPU buffer and writing to CPU buffer
src_vk->Barrier(mCommandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT, false);
dst_vk->Barrier(mCommandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, false);
// Copy
VkBufferCopy copy = {};
copy.srcOffset = 0;
copy.dstOffset = 0;
copy.size = src_vk->GetSize() * src_vk->GetStride();
mComputeSystem->mVkCmdCopyBuffer(mCommandBuffer, src_vk->GetBufferGPU(), dst_vk->GetBufferCPU(), 1, &copy);
// Barrier to indicate that CPU can read from the buffer
dst_vk->Barrier(mCommandBuffer, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_HOST_READ_BIT, false);
mUsedBuffers.insert(src_vk);
mUsedBuffers.insert(dst_vk);
}
void ComputeQueueVK::Dispatch(uint inThreadGroupsX, uint inThreadGroupsY, uint inThreadGroupsZ)
{
if (!BeginCommandBuffer())
return;
mComputeSystem->mVkCmdBindPipeline(mCommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, mShader->GetPipeline());
VkDevice device = mComputeSystem->GetDevice();
const Array<VkDescriptorSetLayoutBinding> &ds_bindings = mShader->GetLayoutBindings();
if (!ds_bindings.empty())
{
// Create a descriptor set
VkDescriptorSetAllocateInfo alloc_info = {};
alloc_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
alloc_info.descriptorPool = mDescriptorPool;
alloc_info.descriptorSetCount = 1;
VkDescriptorSetLayout ds_layout = mShader->GetDescriptorSetLayout();
alloc_info.pSetLayouts = &ds_layout;
VkDescriptorSet descriptor_set;
if (VKFailed(mComputeSystem->mVkAllocateDescriptorSets(device, &alloc_info, &descriptor_set)))
return;
// Write the values to the descriptor set
Array<VkWriteDescriptorSet> writes;
writes.reserve(ds_bindings.size());
for (uint32 i = 0; i < (uint32)ds_bindings.size(); ++i)
{
VkWriteDescriptorSet w = {};
w.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
w.dstSet = descriptor_set;
w.dstBinding = ds_bindings[i].binding;
w.dstArrayElement = 0;
w.descriptorCount = ds_bindings[i].descriptorCount;
w.descriptorType = ds_bindings[i].descriptorType;
w.pBufferInfo = &mBufferInfos[i];
writes.push_back(w);
}
mComputeSystem->mVkUpdateDescriptorSets(device, (uint32)writes.size(), writes.data(), 0, nullptr);
// Bind the descriptor set
mComputeSystem->mVkCmdBindDescriptorSets(mCommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, mShader->GetPipelineLayout(), 0, 1, &descriptor_set, 0, nullptr);
}
mComputeSystem->mVkCmdDispatch(mCommandBuffer, inThreadGroupsX, inThreadGroupsY, inThreadGroupsZ);
}
void ComputeQueueVK::Execute()
{
// End command buffer
if (!mCommandBufferRecording)
return;
if (VKFailed(mComputeSystem->mVkEndCommandBuffer(mCommandBuffer)))
return;
mCommandBufferRecording = false;
// Reset fence
VkDevice device = mComputeSystem->GetDevice();
if (VKFailed(mComputeSystem->mVkResetFences(device, 1, &mFence)))
return;
// Submit
VkSubmitInfo submit = {};
submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submit.commandBufferCount = 1;
submit.pCommandBuffers = &mCommandBuffer;
if (VKFailed(mComputeSystem->mVkQueueSubmit(mQueue, 1, &submit, mFence)))
return;
// Clear the current shader
mShader = nullptr;
// Mark that we're executing
mIsExecuting = true;
}
void ComputeQueueVK::Wait()
{
if (!mIsExecuting)
return;
// Wait for the work to complete
VkDevice device = mComputeSystem->GetDevice();
if (VKFailed(mComputeSystem->mVkWaitForFences(device, 1, &mFence, VK_TRUE, UINT64_MAX)))
return;
// Reset command buffer so it can be reused
if (mCommandBuffer != VK_NULL_HANDLE)
mComputeSystem->mVkResetCommandBuffer(mCommandBuffer, 0);
// Allow reusing the descriptors for next run
mComputeSystem->mVkResetDescriptorPool(device, mDescriptorPool, 0);
// Buffers can be freed now
mUsedBuffers.clear();
// Free delayed buffers
for (BufferVK &buffer : mDelayedFreedBuffers)
mComputeSystem->FreeBuffer(buffer);
mDelayedFreedBuffers.clear();
mIsExecuting = false;
}
JPH_NAMESPACE_END
#endif // JPH_USE_VK

View file

@ -0,0 +1,66 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/Compute/ComputeQueue.h>
#ifdef JPH_USE_VK
#include <Jolt/Compute/VK/ComputeShaderVK.h>
#include <Jolt/Compute/VK/BufferVK.h>
#include <Jolt/Core/UnorderedMap.h>
#include <Jolt/Core/UnorderedSet.h>
JPH_NAMESPACE_BEGIN
class ComputeSystemVK;
class ComputeBufferVK;
/// A command queue for Vulkan for executing compute workloads on the GPU.
class JPH_EXPORT ComputeQueueVK final : public ComputeQueue
{
public:
JPH_OVERRIDE_NEW_DELETE
/// Constructor / Destructor
explicit ComputeQueueVK(ComputeSystemVK *inComputeSystem) : mComputeSystem(inComputeSystem) { }
virtual ~ComputeQueueVK() override;
/// Initialize the queue
bool Initialize(uint32 inComputeQueueIndex, ComputeQueueResult &outResult);
// See: ComputeQueue
virtual void SetShader(const ComputeShader *inShader) override;
virtual void SetConstantBuffer(const char *inName, const ComputeBuffer *inBuffer) override;
virtual void SetBuffer(const char *inName, const ComputeBuffer *inBuffer) override;
virtual void SetRWBuffer(const char *inName, ComputeBuffer *inBuffer, EBarrier inBarrier = EBarrier::Yes) override;
virtual void ScheduleReadback(ComputeBuffer *inDst, const ComputeBuffer *inSrc) override;
virtual void Dispatch(uint inThreadGroupsX, uint inThreadGroupsY, uint inThreadGroupsZ) override;
virtual void Execute() override;
virtual void Wait() override;
private:
bool BeginCommandBuffer();
// Copy the CPU buffer to the GPU buffer if needed
void SyncCPUToGPU(const ComputeBufferVK *inBuffer);
ComputeSystemVK * mComputeSystem;
VkQueue mQueue = VK_NULL_HANDLE;
VkCommandPool mCommandPool = VK_NULL_HANDLE;
VkDescriptorPool mDescriptorPool = VK_NULL_HANDLE;
VkCommandBuffer mCommandBuffer = VK_NULL_HANDLE;
bool mCommandBufferRecording = false; ///< If we are currently recording commands into the command buffer
VkFence mFence = VK_NULL_HANDLE;
bool mIsExecuting = false; ///< If Execute has been called and we are waiting for it to finish
RefConst<ComputeShaderVK> mShader; ///< Shader that has been activated
Array<VkDescriptorBufferInfo> mBufferInfos; ///< List of parameters that will be sent to the current shader
UnorderedSet<RefConst<ComputeBuffer>> mUsedBuffers; ///< Buffers that are in use by the current execution, these will be retained until execution is finished so that we don't free buffers that are in use
Array<BufferVK> mDelayedFreedBuffers; ///< Hardware buffers that need to be freed after execution is done
};
JPH_NAMESPACE_END
#endif // JPH_USE_VK

View file

@ -0,0 +1,237 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <Jolt/Jolt.h>
#ifdef JPH_USE_VK
#include <Jolt/Compute/VK/ComputeShaderVK.h>
#include <Jolt/Compute/VK/ComputeSystemVK.h>
JPH_NAMESPACE_BEGIN
ComputeShaderVK::~ComputeShaderVK()
{
VkDevice device = mComputeSystem->GetDevice();
if (mShaderModule != VK_NULL_HANDLE)
mComputeSystem->mVkDestroyShaderModule(device, mShaderModule, nullptr);
if (mDescriptorSetLayout != VK_NULL_HANDLE)
mComputeSystem->mVkDestroyDescriptorSetLayout(device, mDescriptorSetLayout, nullptr);
if (mPipelineLayout != VK_NULL_HANDLE)
mComputeSystem->mVkDestroyPipelineLayout(device, mPipelineLayout, nullptr);
if (mPipeline != VK_NULL_HANDLE)
mComputeSystem->mVkDestroyPipeline(device, mPipeline, nullptr);
}
bool ComputeShaderVK::Initialize(const Array<uint8> &inSPVCode, VkBuffer inDummyBuffer, ComputeShaderResult &outResult)
{
const uint32 *spv_words = reinterpret_cast<const uint32 *>(inSPVCode.data());
size_t spv_word_count = inSPVCode.size() / sizeof(uint32);
// Minimal SPIR-V parser to extract name to binding info
UnorderedMap<uint32, String> id_to_name;
UnorderedMap<uint32, uint32> id_to_binding;
UnorderedMap<uint32, VkDescriptorType> id_to_descriptor_type;
UnorderedMap<uint32, uint32> pointer_to_pointee;
UnorderedMap<uint32, uint32> var_to_ptr_type;
size_t i = 5; // Skip 5 word header
while (i < spv_word_count)
{
// Parse next word
uint32 word = spv_words[i];
uint16 opcode = uint16(word & 0xffff);
uint16 word_count = uint16(word >> 16);
if (word_count == 0 || i + word_count > spv_word_count)
break;
switch (opcode)
{
case 5: // OpName
if (word_count >= 2)
{
uint32 target_id = spv_words[i + 1];
const char* name = reinterpret_cast<const char*>(&spv_words[i + 2]);
if (*name != 0)
id_to_name.insert({ target_id, name });
}
break;
case 16: // OpExecutionMode
if (word_count >= 6)
{
uint32 execution_mode = spv_words[i + 2];
if (execution_mode == 17) // LocalSize
{
// Assert that the group size provided matches the one in the shader
JPH_ASSERT(GetGroupSizeX() == spv_words[i + 3], "Group size X mismatch");
JPH_ASSERT(GetGroupSizeY() == spv_words[i + 4], "Group size Y mismatch");
JPH_ASSERT(GetGroupSizeZ() == spv_words[i + 5], "Group size Z mismatch");
}
}
break;
case 32: // OpTypePointer
if (word_count >= 4)
{
uint32 result_id = spv_words[i + 1];
uint32 type_id = spv_words[i + 3];
pointer_to_pointee.insert({ result_id, type_id });
}
break;
case 59: // OpVariable
if (word_count >= 3)
{
uint32 ptr_type_id = spv_words[i + 1];
uint32 result_id = spv_words[i + 2];
var_to_ptr_type.insert({ result_id, ptr_type_id });
}
break;
case 71: // OpDecorate
if (word_count >= 3)
{
uint32 target_id = spv_words[i + 1];
uint32 decoration = spv_words[i + 2];
if (decoration == 2) // Block
{
id_to_descriptor_type.insert({ target_id, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER });
}
else if (decoration == 3) // BufferBlock
{
id_to_descriptor_type.insert({ target_id, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER });
}
else if (decoration == 33 && word_count >= 4) // Binding
{
uint32 binding = spv_words[i + 3];
id_to_binding.insert({ target_id, binding });
}
}
break;
default:
break;
}
i += word_count;
}
// Build name to binding map
UnorderedMap<String, std::pair<uint32, VkDescriptorType>> name_to_binding;
for (const UnorderedMap<uint32, uint32>::value_type &entry : id_to_binding)
{
uint32 target_id = entry.first;
uint32 binding = entry.second;
// Get the name of the variable
UnorderedMap<uint32, String>::const_iterator it_name = id_to_name.find(target_id);
if (it_name != id_to_name.end())
{
// Find variable that links to the target
UnorderedMap<uint32, uint32>::const_iterator it_var_ptr = var_to_ptr_type.find(target_id);
if (it_var_ptr != var_to_ptr_type.end())
{
// Find type pointed at
uint32 ptr_type = it_var_ptr->second;
UnorderedMap<uint32, uint32>::const_iterator it_pointee = pointer_to_pointee.find(ptr_type);
if (it_pointee != pointer_to_pointee.end())
{
uint32 pointee_type = it_pointee->second;
// Find descriptor type
UnorderedMap<uint32, VkDescriptorType>::iterator it_descriptor_type = id_to_descriptor_type.find(pointee_type);
VkDescriptorType descriptor_type = it_descriptor_type != id_to_descriptor_type.end() ? it_descriptor_type->second : VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
name_to_binding.insert({ it_name->second, { binding, descriptor_type } });
continue;
}
}
}
}
VkDevice device = mComputeSystem->GetDevice();
// Create layout bindings and buffer infos
if (!name_to_binding.empty())
{
mLayoutBindings.reserve(name_to_binding.size());
mBufferInfos.reserve(name_to_binding.size());
mBindingNames.reserve(name_to_binding.size());
for (const UnorderedMap<String, std::pair<uint32, VkDescriptorType>>::value_type &b : name_to_binding)
{
const String &name = b.first;
uint binding = b.second.first;
VkDescriptorType descriptor_type = b.second.second;
VkDescriptorSetLayoutBinding l = {};
l.binding = binding;
l.descriptorCount = 1;
l.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
l.descriptorType = descriptor_type;
mLayoutBindings.push_back(l);
mBindingNames.push_back(name); // Add all strings to a pool to keep them alive
mNameToBufferInfoIndex[string_view(mBindingNames.back())] = (uint32)mBufferInfos.size();
VkDescriptorBufferInfo bi = {};
bi.offset = 0;
bi.range = VK_WHOLE_SIZE;
bi.buffer = inDummyBuffer; // Avoid: The Vulkan spec states: If the nullDescriptor feature is not enabled, buffer must not be VK_NULL_HANDLE
mBufferInfos.push_back(bi);
}
// Create descriptor set layout
VkDescriptorSetLayoutCreateInfo layout_info = {};
layout_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layout_info.bindingCount = (uint32)mLayoutBindings.size();
layout_info.pBindings = mLayoutBindings.data();
if (VKFailed(mComputeSystem->mVkCreateDescriptorSetLayout(device, &layout_info, nullptr, &mDescriptorSetLayout), outResult))
return false;
}
// Create pipeline layout
VkPipelineLayoutCreateInfo pl_info = {};
pl_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pl_info.setLayoutCount = mDescriptorSetLayout != VK_NULL_HANDLE ? 1 : 0;
pl_info.pSetLayouts = mDescriptorSetLayout != VK_NULL_HANDLE ? &mDescriptorSetLayout : nullptr;
if (VKFailed(mComputeSystem->mVkCreatePipelineLayout(device, &pl_info, nullptr, &mPipelineLayout), outResult))
return false;
// Create shader module
VkShaderModuleCreateInfo create_info = {};
create_info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
create_info.codeSize = inSPVCode.size();
create_info.pCode = spv_words;
if (VKFailed(mComputeSystem->mVkCreateShaderModule(device, &create_info, nullptr, &mShaderModule), outResult))
return false;
// Create compute pipeline
VkComputePipelineCreateInfo pipe_info = {};
pipe_info.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO;
pipe_info.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
pipe_info.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
pipe_info.stage.module = mShaderModule;
pipe_info.stage.pName = "main";
pipe_info.layout = mPipelineLayout;
if (VKFailed(mComputeSystem->mVkCreateComputePipelines(device, VK_NULL_HANDLE, 1, &pipe_info, nullptr, &mPipeline), outResult))
return false;
return true;
}
uint32 ComputeShaderVK::NameToBufferInfoIndex(const char *inName) const
{
UnorderedMap<string_view, uint>::const_iterator it = mNameToBufferInfoIndex.find(inName);
JPH_ASSERT(it != mNameToBufferInfoIndex.end());
return it->second;
}
JPH_NAMESPACE_END
#endif // JPH_USE_VK

View file

@ -0,0 +1,55 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/Compute/ComputeShader.h>
#ifdef JPH_USE_VK
#include <Jolt/Compute/VK/IncludeVK.h>
#include <Jolt/Core/UnorderedMap.h>
JPH_NAMESPACE_BEGIN
class ComputeSystemVK;
/// Compute shader handle for Vulkan
class JPH_EXPORT ComputeShaderVK : public ComputeShader
{
public:
JPH_OVERRIDE_NEW_DELETE
/// Constructor / destructor
ComputeShaderVK(ComputeSystemVK *inComputeSystem, uint32 inGroupSizeX, uint32 inGroupSizeY, uint32 inGroupSizeZ) : ComputeShader(inGroupSizeX, inGroupSizeY, inGroupSizeZ), mComputeSystem(inComputeSystem) { }
virtual ~ComputeShaderVK() override;
/// Initialize from SPIR-V code
bool Initialize(const Array<uint8> &inSPVCode, VkBuffer inDummyBuffer, ComputeShaderResult &outResult);
/// Get index of parameter in buffer infos
uint32 NameToBufferInfoIndex(const char *inName) const;
/// Getters
VkPipeline GetPipeline() const { return mPipeline; }
VkPipelineLayout GetPipelineLayout() const { return mPipelineLayout; }
VkDescriptorSetLayout GetDescriptorSetLayout() const { return mDescriptorSetLayout; }
const Array<VkDescriptorSetLayoutBinding> &GetLayoutBindings() const { return mLayoutBindings; }
const Array<VkDescriptorBufferInfo> &GetBufferInfos() const { return mBufferInfos; }
private:
ComputeSystemVK * mComputeSystem;
VkShaderModule mShaderModule = VK_NULL_HANDLE;
VkPipelineLayout mPipelineLayout = VK_NULL_HANDLE;
VkPipeline mPipeline = VK_NULL_HANDLE;
VkDescriptorSetLayout mDescriptorSetLayout = VK_NULL_HANDLE;
Array<String> mBindingNames; ///< A list of binding names, mNameToBufferInfoIndex points to these strings
UnorderedMap<string_view, uint32> mNameToBufferInfoIndex; ///< Binding name to buffer index, using a string_view so we can do find() without an allocation
Array<VkDescriptorSetLayoutBinding> mLayoutBindings;
Array<VkDescriptorBufferInfo> mBufferInfos;
};
JPH_NAMESPACE_END
#endif // JPH_USE_VK

View file

@ -0,0 +1,163 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <Jolt/Jolt.h>
#ifdef JPH_USE_VK
#include <Jolt/Compute/VK/ComputeSystemVK.h>
#include <Jolt/Compute/VK/ComputeShaderVK.h>
#include <Jolt/Compute/VK/ComputeBufferVK.h>
#include <Jolt/Compute/VK/ComputeQueueVK.h>
JPH_NAMESPACE_BEGIN
JPH_IMPLEMENT_RTTI_ABSTRACT(ComputeSystemVK)
{
JPH_ADD_BASE_CLASS(ComputeSystemVK, ComputeSystem)
}
bool ComputeSystemVK::Initialize(VkPhysicalDevice inPhysicalDevice, PFN_vkGetDeviceProcAddr inVkGetDeviceProcAddr, VkDevice inDevice, uint32 inComputeQueueIndex, ComputeSystemResult &outResult)
{
mPhysicalDevice = inPhysicalDevice;
mDevice = inDevice;
mComputeQueueIndex = inComputeQueueIndex;
// Load Vulkan device functions
#define JPH_LOAD_VK(name) mVk##name = reinterpret_cast<PFN_vk##name>(reinterpret_cast<void *>(inVkGetDeviceProcAddr(mDevice, "vk" #name))); JPH_ASSERT(mVk##name != nullptr)
JPH_LOAD_VK(AllocateCommandBuffers);
JPH_LOAD_VK(AllocateDescriptorSets);
JPH_LOAD_VK(AllocateMemory);
JPH_LOAD_VK(BeginCommandBuffer);
JPH_LOAD_VK(BindBufferMemory);
JPH_LOAD_VK(CmdBindDescriptorSets);
JPH_LOAD_VK(CmdBindPipeline);
JPH_LOAD_VK(CmdCopyBuffer);
JPH_LOAD_VK(CmdDispatch);
JPH_LOAD_VK(CmdPipelineBarrier);
JPH_LOAD_VK(CreateBuffer);
JPH_LOAD_VK(CreateCommandPool);
JPH_LOAD_VK(CreateComputePipelines);
JPH_LOAD_VK(CreateDescriptorPool);
JPH_LOAD_VK(CreateDescriptorSetLayout);
JPH_LOAD_VK(CreateFence);
JPH_LOAD_VK(CreatePipelineLayout);
JPH_LOAD_VK(CreateShaderModule);
JPH_LOAD_VK(DestroyBuffer);
JPH_LOAD_VK(DestroyCommandPool);
JPH_LOAD_VK(DestroyDescriptorPool);
JPH_LOAD_VK(DestroyDescriptorSetLayout);
JPH_LOAD_VK(DestroyDevice);
JPH_LOAD_VK(DestroyFence);
JPH_LOAD_VK(DestroyPipeline);
JPH_LOAD_VK(DestroyPipelineLayout);
JPH_LOAD_VK(DestroyShaderModule);
JPH_LOAD_VK(DeviceWaitIdle);
JPH_LOAD_VK(EndCommandBuffer);
JPH_LOAD_VK(FreeCommandBuffers);
JPH_LOAD_VK(FreeMemory);
JPH_LOAD_VK(GetBufferMemoryRequirements);
JPH_LOAD_VK(GetDeviceQueue);
JPH_LOAD_VK(MapMemory);
JPH_LOAD_VK(QueueSubmit);
JPH_LOAD_VK(ResetCommandBuffer);
JPH_LOAD_VK(ResetDescriptorPool);
JPH_LOAD_VK(ResetFences);
JPH_LOAD_VK(UnmapMemory);
JPH_LOAD_VK(UpdateDescriptorSets);
JPH_LOAD_VK(WaitForFences);
#undef JPH_LOAD_VK
// Get function to set a debug name
mVkSetDebugUtilsObjectNameEXT = reinterpret_cast<PFN_vkSetDebugUtilsObjectNameEXT>(reinterpret_cast<void *>(inVkGetDeviceProcAddr(mDevice, "vkSetDebugUtilsObjectNameEXT")));
if (!InitializeMemory())
{
outResult.SetError("Failed to initialize memory subsystem");
return false;
}
// Create the dummy buffer. This is used to bind to shaders for which we have no buffer. We can't rely on VK_EXT_robustness2 being available to set nullDescriptor = VK_TRUE (it is unavailable on macOS).
if (!CreateBuffer(1024, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, mDummyBuffer))
{
outResult.SetError("Failed to create dummy buffer");
return false;
}
return true;
}
void ComputeSystemVK::Shutdown()
{
if (mDevice != VK_NULL_HANDLE)
mVkDeviceWaitIdle(mDevice);
// Free the dummy buffer
FreeBuffer(mDummyBuffer);
ShutdownMemory();
}
ComputeShaderResult ComputeSystemVK::CreateComputeShader(const char *inName, uint32 inGroupSizeX, uint32 inGroupSizeY, uint32 inGroupSizeZ)
{
ComputeShaderResult result;
// Read shader source file
Array<uint8> data;
String file_name = String(inName) + ".spv";
String error;
if (!mShaderLoader(file_name.c_str(), data, error))
{
result.SetError(error);
return result;
}
Ref<ComputeShaderVK> shader = new ComputeShaderVK(this, inGroupSizeX, inGroupSizeY, inGroupSizeZ);
if (!shader->Initialize(data, mDummyBuffer.mBuffer, result))
return result;
// Name the pipeline so we can easily find it in a profile
if (mVkSetDebugUtilsObjectNameEXT != nullptr)
{
VkDebugUtilsObjectNameInfoEXT info = {};
info.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT;
info.pNext = nullptr;
info.objectType = VK_OBJECT_TYPE_PIPELINE;
info.objectHandle = (uint64)shader->GetPipeline();
info.pObjectName = inName;
mVkSetDebugUtilsObjectNameEXT(mDevice, &info);
}
result.Set(shader.GetPtr());
return result;
}
ComputeBufferResult ComputeSystemVK::CreateComputeBuffer(ComputeBuffer::EType inType, uint64 inSize, uint inStride, const void *inData)
{
ComputeBufferResult result;
Ref<ComputeBufferVK> buffer = new ComputeBufferVK(this, inType, inSize, inStride);
if (!buffer->Initialize(inData))
{
result.SetError("Failed to create compute buffer");
return result;
}
result.Set(buffer.GetPtr());
return result;
}
ComputeQueueResult ComputeSystemVK::CreateComputeQueue()
{
ComputeQueueResult result;
Ref<ComputeQueueVK> q = new ComputeQueueVK(this);
if (!q->Initialize(mComputeQueueIndex, result))
return result;
result.Set(q.GetPtr());
return result;
}
JPH_NAMESPACE_END
#endif // JPH_USE_VK

View file

@ -0,0 +1,100 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/Compute/ComputeSystem.h>
#ifdef JPH_USE_VK
#include <Jolt/Compute/VK/ComputeQueueVK.h>
JPH_NAMESPACE_BEGIN
/// Interface to run a workload on the GPU using Vulkan.
/// Minimal implementation that can integrate with your own Vulkan setup.
class JPH_EXPORT ComputeSystemVK : public ComputeSystem
{
public:
JPH_DECLARE_RTTI_ABSTRACT(JPH_EXPORT, ComputeSystemVK)
// Initialize / shutdown the compute system
bool Initialize(VkPhysicalDevice inPhysicalDevice, PFN_vkGetDeviceProcAddr inVkGetDeviceProcAddr, VkDevice inDevice, uint32 inComputeQueueIndex, ComputeSystemResult &outResult);
void Shutdown();
// See: ComputeSystem
virtual ComputeShaderResult CreateComputeShader(const char *inName, uint32 inGroupSizeX, uint32 inGroupSizeY, uint32 inGroupSizeZ) override;
virtual ComputeBufferResult CreateComputeBuffer(ComputeBuffer::EType inType, uint64 inSize, uint inStride, const void *inData = nullptr) override;
virtual ComputeQueueResult CreateComputeQueue() override;
/// Access to the Vulkan device
VkDevice GetDevice() const { return mDevice; }
/// Allow the application to override buffer creation and memory mapping in case it uses its own allocator
virtual bool CreateBuffer(VkDeviceSize inSize, VkBufferUsageFlags inUsage, VkMemoryPropertyFlags inProperties, BufferVK &outBuffer) = 0;
virtual void FreeBuffer(BufferVK &ioBuffer) = 0;
virtual void * MapBuffer(BufferVK &ioBuffer) = 0;
virtual void UnmapBuffer(BufferVK &ioBuffer) = 0;
// Vulkan device function pointers
PFN_vkAllocateCommandBuffers mVkAllocateCommandBuffers = nullptr;
PFN_vkAllocateDescriptorSets mVkAllocateDescriptorSets = nullptr;
PFN_vkAllocateMemory mVkAllocateMemory = nullptr;
PFN_vkBeginCommandBuffer mVkBeginCommandBuffer = nullptr;
PFN_vkBindBufferMemory mVkBindBufferMemory = nullptr;
PFN_vkCmdBindDescriptorSets mVkCmdBindDescriptorSets = nullptr;
PFN_vkCmdBindPipeline mVkCmdBindPipeline = nullptr;
PFN_vkCmdCopyBuffer mVkCmdCopyBuffer = nullptr;
PFN_vkCmdDispatch mVkCmdDispatch = nullptr;
PFN_vkCmdPipelineBarrier mVkCmdPipelineBarrier = nullptr;
PFN_vkCreateBuffer mVkCreateBuffer = nullptr;
PFN_vkCreateCommandPool mVkCreateCommandPool = nullptr;
PFN_vkCreateComputePipelines mVkCreateComputePipelines = nullptr;
PFN_vkCreateDescriptorPool mVkCreateDescriptorPool = nullptr;
PFN_vkCreateDescriptorSetLayout mVkCreateDescriptorSetLayout = nullptr;
PFN_vkCreateFence mVkCreateFence = nullptr;
PFN_vkCreatePipelineLayout mVkCreatePipelineLayout = nullptr;
PFN_vkCreateShaderModule mVkCreateShaderModule = nullptr;
PFN_vkDestroyBuffer mVkDestroyBuffer = nullptr;
PFN_vkDestroyCommandPool mVkDestroyCommandPool = nullptr;
PFN_vkDestroyDescriptorPool mVkDestroyDescriptorPool = nullptr;
PFN_vkDestroyDescriptorSetLayout mVkDestroyDescriptorSetLayout = nullptr;
PFN_vkDestroyDevice mVkDestroyDevice = nullptr;
PFN_vkDestroyFence mVkDestroyFence = nullptr;
PFN_vkDestroyPipeline mVkDestroyPipeline = nullptr;
PFN_vkDestroyPipelineLayout mVkDestroyPipelineLayout = nullptr;
PFN_vkDestroyShaderModule mVkDestroyShaderModule = nullptr;
PFN_vkDeviceWaitIdle mVkDeviceWaitIdle = nullptr;
PFN_vkEndCommandBuffer mVkEndCommandBuffer = nullptr;
PFN_vkFreeCommandBuffers mVkFreeCommandBuffers = nullptr;
PFN_vkFreeMemory mVkFreeMemory = nullptr;
PFN_vkGetBufferMemoryRequirements mVkGetBufferMemoryRequirements = nullptr;
PFN_vkGetDeviceQueue mVkGetDeviceQueue = nullptr;
PFN_vkMapMemory mVkMapMemory = nullptr;
PFN_vkQueueSubmit mVkQueueSubmit = nullptr;
PFN_vkResetCommandBuffer mVkResetCommandBuffer = nullptr;
PFN_vkResetDescriptorPool mVkResetDescriptorPool = nullptr;
PFN_vkResetFences mVkResetFences = nullptr;
PFN_vkSetDebugUtilsObjectNameEXT mVkSetDebugUtilsObjectNameEXT = nullptr;
PFN_vkUnmapMemory mVkUnmapMemory = nullptr;
PFN_vkUpdateDescriptorSets mVkUpdateDescriptorSets = nullptr;
PFN_vkWaitForFences mVkWaitForFences = nullptr;
protected:
/// Initialize / shutdown the memory subsystem
virtual bool InitializeMemory() = 0;
virtual void ShutdownMemory() = 0;
VkPhysicalDevice mPhysicalDevice = VK_NULL_HANDLE;
VkDevice mDevice = VK_NULL_HANDLE;
uint32 mComputeQueueIndex = 0;
private:
// Buffer that can be bound when we have no buffer
BufferVK mDummyBuffer;
};
JPH_NAMESPACE_END
#endif // JPH_USE_VK

View file

@ -0,0 +1,398 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <Jolt/Jolt.h>
#ifdef JPH_USE_VK
#include <Jolt/Compute/VK/ComputeSystemVKImpl.h>
#include <Jolt/Core/QuickSort.h>
#include <Jolt/Core/IncludeWindows.h>
#if defined(JPH_PLATFORM_LINUX) || defined(JPH_PLATFORM_ANDROID) ||defined(JPH_PLATFORM_MACOS)
#include <dlfcn.h>
#endif
JPH_NAMESPACE_BEGIN
JPH_IMPLEMENT_RTTI_VIRTUAL(ComputeSystemVKImpl)
{
JPH_ADD_BASE_CLASS(ComputeSystemVKImpl, ComputeSystemVKWithAllocator)
}
#ifdef JPH_DEBUG
static VKAPI_ATTR VkBool32 VKAPI_CALL sVulkanDebugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT inSeverity, [[maybe_unused]] VkDebugUtilsMessageTypeFlagsEXT inType, const VkDebugUtilsMessengerCallbackDataEXT *inCallbackData, [[maybe_unused]] void *inUserData)
{
if (inSeverity & (VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT))
Trace("VK: %s", inCallbackData->pMessage);
JPH_ASSERT((inSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) == 0);
return VK_FALSE;
}
#endif // JPH_DEBUG
ComputeSystemVKImpl::~ComputeSystemVKImpl()
{
ComputeSystemVK::Shutdown();
if (mDevice != VK_NULL_HANDLE && mVkDestroyDevice != nullptr)
mVkDestroyDevice(mDevice, nullptr);
#ifdef JPH_DEBUG
if (mInstance != VK_NULL_HANDLE && mDebugMessenger != VK_NULL_HANDLE && mVkDestroyDebugUtilsMessengerEXT != nullptr)
mVkDestroyDebugUtilsMessengerEXT(mInstance, mDebugMessenger, nullptr);
#endif
if (mInstance != VK_NULL_HANDLE && mVkDestroyInstance != nullptr)
mVkDestroyInstance(mInstance, nullptr);
}
bool ComputeSystemVKImpl::Initialize(ComputeSystemResult &outResult)
{
#ifdef JPH_PLATFORM_WINDOWS
HMODULE module = LoadLibraryA("vulkan-1.dll");
if (!module)
{
outResult.SetError("Failed to load vulkan-1.dll");
return false;
}
mVkGetInstanceProcAddr = reinterpret_cast<PFN_vkGetInstanceProcAddr>(reinterpret_cast<void *>(GetProcAddress(module, "vkGetInstanceProcAddr")));
#elif defined(JPH_PLATFORM_LINUX) || defined(JPH_PLATFORM_ANDROID)
void *library = dlopen("libvulkan.so.1", RTLD_NOW | RTLD_LOCAL);
if (!library)
library = dlopen("libvulkan.so", RTLD_NOW | RTLD_LOCAL);
if (!library)
{
outResult.SetError("Failed to load libvulkan.so.1 or libvulkan.so");
return false;
}
mVkGetInstanceProcAddr = reinterpret_cast<PFN_vkGetInstanceProcAddr>(dlsym(library, "vkGetInstanceProcAddr"));
#elif defined(JPH_PLATFORM_MACOS)
void *library = dlopen("libvulkan.1.dylib", RTLD_NOW | RTLD_LOCAL);
if (!library)
library = dlopen("libvulkan.dylib", RTLD_NOW | RTLD_LOCAL);
if (!library)
{
outResult.SetError("Failed to load libvulkan.1.dylib or libvulkan.dylib");
return false;
}
mVkGetInstanceProcAddr = reinterpret_cast<PFN_vkGetInstanceProcAddr>(dlsym(library, "vkGetInstanceProcAddr"));
#else
#error "Unsupported platform"
#endif
// Check vkGetInstanceProcAddr
if (mVkGetInstanceProcAddr == nullptr)
{
outResult.SetError("Failed to get vkGetInstanceProcAddr");
return false;
}
// Required instance extensions
Array<const char *> required_instance_extensions;
required_instance_extensions.push_back(VK_KHR_SURFACE_EXTENSION_NAME);
required_instance_extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
#ifdef JPH_PLATFORM_MACOS
required_instance_extensions.push_back("VK_KHR_portability_enumeration");
required_instance_extensions.push_back("VK_KHR_get_physical_device_properties2");
#endif
GetInstanceExtensions(required_instance_extensions);
// Required device extensions
Array<const char *> required_device_extensions;
required_device_extensions.push_back(VK_EXT_SCALAR_BLOCK_LAYOUT_EXTENSION_NAME);
#ifdef JPH_PLATFORM_MACOS
required_device_extensions.push_back("VK_KHR_portability_subset"); // VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME
#endif
GetDeviceExtensions(required_device_extensions);
// Load pre-instance Vulkan functions
#define JPH_LOAD_VK_PRE_INST(name) mVk##name = reinterpret_cast<PFN_vk##name>(reinterpret_cast<void *>(mVkGetInstanceProcAddr(nullptr, "vk" #name))); JPH_ASSERT(mVk##name != nullptr)
JPH_LOAD_VK_PRE_INST(CreateInstance);
JPH_LOAD_VK_PRE_INST(EnumerateInstanceExtensionProperties);
JPH_LOAD_VK_PRE_INST(EnumerateInstanceLayerProperties);
#undef JPH_LOAD_VK_PRE_INST
// Query supported instance extensions
uint32 instance_extension_count = 0;
if (VKFailed(mVkEnumerateInstanceExtensionProperties(nullptr, &instance_extension_count, nullptr), outResult))
return false;
Array<VkExtensionProperties> instance_extensions;
instance_extensions.resize(instance_extension_count);
if (VKFailed(mVkEnumerateInstanceExtensionProperties(nullptr, &instance_extension_count, instance_extensions.data()), outResult))
return false;
// Query supported validation layers
uint32 validation_layer_count;
mVkEnumerateInstanceLayerProperties(&validation_layer_count, nullptr);
Array<VkLayerProperties> validation_layers(validation_layer_count);
mVkEnumerateInstanceLayerProperties(&validation_layer_count, validation_layers.data());
VkApplicationInfo app_info = {};
app_info.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
app_info.apiVersion = VK_API_VERSION_1_1;
// Create Vulkan instance
VkInstanceCreateInfo instance_create_info = {};
instance_create_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
#ifdef JPH_PLATFORM_MACOS
instance_create_info.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR;
#endif
instance_create_info.pApplicationInfo = &app_info;
#ifdef JPH_DEBUG
// Enable validation layer if supported
const char *desired_validation_layers[] = { "VK_LAYER_KHRONOS_validation" };
for (const VkLayerProperties &p : validation_layers)
if (strcmp(desired_validation_layers[0], p.layerName) == 0)
{
instance_create_info.enabledLayerCount = 1;
instance_create_info.ppEnabledLayerNames = desired_validation_layers;
break;
}
// Setup debug messenger callback if the extension is supported
VkDebugUtilsMessengerCreateInfoEXT messenger_create_info = {};
for (const VkExtensionProperties &ext : instance_extensions)
if (strcmp(VK_EXT_DEBUG_UTILS_EXTENSION_NAME, ext.extensionName) == 0)
{
messenger_create_info.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
messenger_create_info.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
messenger_create_info.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
messenger_create_info.pfnUserCallback = sVulkanDebugCallback;
instance_create_info.pNext = &messenger_create_info;
required_instance_extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
break;
}
#endif
instance_create_info.enabledExtensionCount = (uint32)required_instance_extensions.size();
instance_create_info.ppEnabledExtensionNames = required_instance_extensions.data();
if (VKFailed(mVkCreateInstance(&instance_create_info, nullptr, &mInstance), outResult))
return false;
// Load instance-level Vulkan functions
#define JPH_LOAD_VK_INST(name) mVk##name = reinterpret_cast<PFN_vk##name>(reinterpret_cast<void *>(mVkGetInstanceProcAddr(mInstance, "vk" #name))); JPH_ASSERT(mVk##name != nullptr)
JPH_LOAD_VK_INST(CreateDevice);
JPH_LOAD_VK_INST(DestroyInstance);
JPH_LOAD_VK_INST(EnumerateDeviceExtensionProperties);
JPH_LOAD_VK_INST(EnumeratePhysicalDevices);
JPH_LOAD_VK_INST(GetPhysicalDeviceProperties);
JPH_LOAD_VK_INST(GetPhysicalDeviceQueueFamilyProperties);
#undef JPH_LOAD_VK_INST
// Get vkGetDeviceProcAddr
mVkGetDeviceProcAddr = reinterpret_cast<PFN_vkGetDeviceProcAddr>(reinterpret_cast<void *>(mVkGetInstanceProcAddr(mInstance, "vkGetDeviceProcAddr")));
if (mVkGetDeviceProcAddr == nullptr)
{
outResult.SetError("Failed to get vkGetDeviceProcAddr");
return false;
}
#ifdef JPH_DEBUG
// Finalize debug messenger callback
PFN_vkCreateDebugUtilsMessengerEXT vkCreateDebugUtilsMessengerEXT = (PFN_vkCreateDebugUtilsMessengerEXT)(std::uintptr_t)mVkGetInstanceProcAddr(mInstance, "vkCreateDebugUtilsMessengerEXT");
mVkDestroyDebugUtilsMessengerEXT = reinterpret_cast<PFN_vkDestroyDebugUtilsMessengerEXT>(reinterpret_cast<void *>(mVkGetInstanceProcAddr(mInstance, "vkDestroyDebugUtilsMessengerEXT")));
if (vkCreateDebugUtilsMessengerEXT != nullptr && VKFailed(vkCreateDebugUtilsMessengerEXT(mInstance, &messenger_create_info, nullptr, &mDebugMessenger), outResult))
return false;
#endif
// Notify that instance has been created
OnInstanceCreated();
// Select device
uint32 device_count = 0;
if (VKFailed(mVkEnumeratePhysicalDevices(mInstance, &device_count, nullptr), outResult))
return false;
Array<VkPhysicalDevice> devices;
devices.resize(device_count);
if (VKFailed(mVkEnumeratePhysicalDevices(mInstance, &device_count, devices.data()), outResult))
return false;
struct Device
{
VkPhysicalDevice mPhysicalDevice;
String mName;
VkSurfaceFormatKHR mFormat;
uint32 mGraphicsQueueIndex;
uint32 mPresentQueueIndex;
uint32 mComputeQueueIndex;
int mScore;
};
Array<Device> available_devices;
for (VkPhysicalDevice device : devices)
{
// Get device properties
VkPhysicalDeviceProperties properties;
mVkGetPhysicalDeviceProperties(device, &properties);
// Test if it is an appropriate type
int score = 0;
switch (properties.deviceType)
{
case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU:
score = 30;
break;
case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU:
score = 20;
break;
case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU:
score = 10;
break;
case VK_PHYSICAL_DEVICE_TYPE_CPU:
score = 5;
break;
case VK_PHYSICAL_DEVICE_TYPE_OTHER:
case VK_PHYSICAL_DEVICE_TYPE_MAX_ENUM:
continue;
}
// Check if the device supports all our required extensions
uint32 device_extension_count;
mVkEnumerateDeviceExtensionProperties(device, nullptr, &device_extension_count, nullptr);
Array<VkExtensionProperties> available_extensions;
available_extensions.resize(device_extension_count);
mVkEnumerateDeviceExtensionProperties(device, nullptr, &device_extension_count, available_extensions.data());
int found_extensions = 0;
for (const char *required_device_extension : required_device_extensions)
for (const VkExtensionProperties &ext : available_extensions)
if (strcmp(required_device_extension, ext.extensionName) == 0)
{
found_extensions++;
break;
}
if (found_extensions != int(required_device_extensions.size()))
continue;
// Find the right queues
uint32 queue_family_count = 0;
mVkGetPhysicalDeviceQueueFamilyProperties(device, &queue_family_count, nullptr);
Array<VkQueueFamilyProperties> queue_families;
queue_families.resize(queue_family_count);
mVkGetPhysicalDeviceQueueFamilyProperties(device, &queue_family_count, queue_families.data());
uint32 graphics_queue = ~uint32(0);
uint32 present_queue = ~uint32(0);
uint32 compute_queue = ~uint32(0);
for (uint32 i = 0; i < uint32(queue_families.size()); ++i)
{
if (queue_families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT)
{
graphics_queue = i;
if (queue_families[i].queueFlags & VK_QUEUE_COMPUTE_BIT)
compute_queue = i;
}
if (HasPresentSupport(device, i))
present_queue = i;
if (graphics_queue != ~uint32(0) && present_queue != ~uint32(0) && compute_queue != ~uint32(0))
break;
}
if (graphics_queue == ~uint32(0) || present_queue == ~uint32(0) || compute_queue == ~uint32(0))
continue;
// Select surface format
VkSurfaceFormatKHR selected_format = SelectFormat(device);
if (selected_format.format == VK_FORMAT_UNDEFINED)
continue;
// Add the device
available_devices.push_back({ device, properties.deviceName, selected_format, graphics_queue, present_queue, compute_queue, score });
}
if (available_devices.empty())
{
outResult.SetError("No suitable Vulkan device found");
return false;
}
// Sort the devices by score
QuickSort(available_devices.begin(), available_devices.end(), [](const Device &inLHS, const Device &inRHS) {
return inLHS.mScore > inRHS.mScore;
});
const Device &selected_device = available_devices[0];
// Create device
float queue_priority = 1.0f;
VkDeviceQueueCreateInfo queue_create_info[3] = {};
for (VkDeviceQueueCreateInfo &q : queue_create_info)
{
q.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
q.queueCount = 1;
q.pQueuePriorities = &queue_priority;
}
uint32 num_queues = 0;
queue_create_info[num_queues++].queueFamilyIndex = selected_device.mGraphicsQueueIndex;
bool found = false;
for (uint32 i = 0; i < num_queues; ++i)
if (queue_create_info[i].queueFamilyIndex == selected_device.mPresentQueueIndex)
{
found = true;
break;
}
if (!found)
queue_create_info[num_queues++].queueFamilyIndex = selected_device.mPresentQueueIndex;
found = false;
for (uint32 i = 0; i < num_queues; ++i)
if (queue_create_info[i].queueFamilyIndex == selected_device.mComputeQueueIndex)
{
found = true;
break;
}
if (!found)
queue_create_info[num_queues++].queueFamilyIndex = selected_device.mComputeQueueIndex;
VkPhysicalDeviceScalarBlockLayoutFeatures enable_scalar_block = {};
enable_scalar_block.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SCALAR_BLOCK_LAYOUT_FEATURES;
enable_scalar_block.scalarBlockLayout = VK_TRUE;
VkPhysicalDeviceFeatures2 enabled_features2 = {};
enabled_features2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
GetEnabledFeatures(enabled_features2);
enable_scalar_block.pNext = enabled_features2.pNext;
enabled_features2.pNext = &enable_scalar_block;
VkDeviceCreateInfo device_create_info = {};
device_create_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
device_create_info.queueCreateInfoCount = num_queues;
device_create_info.pQueueCreateInfos = queue_create_info;
device_create_info.enabledExtensionCount = uint32(required_device_extensions.size());
device_create_info.ppEnabledExtensionNames = required_device_extensions.data();
device_create_info.pNext = &enabled_features2;
device_create_info.pEnabledFeatures = nullptr;
VkDevice device = VK_NULL_HANDLE;
if (VKFailed(mVkCreateDevice(selected_device.mPhysicalDevice, &device_create_info, nullptr, &device), outResult))
return false;
// Store selected format
mSelectedFormat = selected_device.mFormat;
// Initialize the compute system (loads device-level functions)
if (!ComputeSystemVKWithAllocator::Initialize(mInstance, selected_device.mPhysicalDevice, mVkGetInstanceProcAddr, mVkGetDeviceProcAddr, device, selected_device.mComputeQueueIndex, outResult))
return false;
// Get the queues
mGraphicsQueueIndex = selected_device.mGraphicsQueueIndex;
mPresentQueueIndex = selected_device.mPresentQueueIndex;
mVkGetDeviceQueue(mDevice, mGraphicsQueueIndex, 0, &mGraphicsQueue);
mVkGetDeviceQueue(mDevice, mPresentQueueIndex, 0, &mPresentQueue);
return true;
}
ComputeSystemResult CreateComputeSystemVK()
{
ComputeSystemResult result;
Ref<ComputeSystemVKImpl> compute = new ComputeSystemVKImpl;
if (!compute->Initialize(result))
return result;
result.Set(compute.GetPtr());
return result;
}
JPH_NAMESPACE_END
#endif // JPH_USE_VK

View file

@ -0,0 +1,74 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#ifdef JPH_USE_VK
#include <Jolt/Compute/VK/ComputeSystemVKWithAllocator.h>
JPH_NAMESPACE_BEGIN
/// Implementation of ComputeSystemVK that fully initializes Vulkan
class JPH_EXPORT ComputeSystemVKImpl : public ComputeSystemVKWithAllocator
{
public:
JPH_DECLARE_RTTI_VIRTUAL(JPH_EXPORT, ComputeSystemVKImpl)
/// Destructor
virtual ~ComputeSystemVKImpl() override;
/// Initialize the compute system
bool Initialize(ComputeSystemResult &outResult);
protected:
/// Override to perform actions once the instance has been created
virtual void OnInstanceCreated() { /* Do nothing */ }
/// Override to add platform specific instance extensions
virtual void GetInstanceExtensions(Array<const char *> &outExtensions) { /* Add nothing */ }
/// Override to add platform specific device extensions
virtual void GetDeviceExtensions(Array<const char *> &outExtensions) { /* Add nothing */ }
/// Override to enable specific features
virtual void GetEnabledFeatures(VkPhysicalDeviceFeatures2 &ioFeatures) { /* Add nothing */ }
/// Override to check for present support on a given device and queue family
virtual bool HasPresentSupport(VkPhysicalDevice inDevice, uint32 inQueueFamilyIndex) { return true; }
/// Override to select the surface format
virtual VkSurfaceFormatKHR SelectFormat(VkPhysicalDevice inDevice) { return { VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR }; }
VkInstance mInstance = VK_NULL_HANDLE;
#ifdef JPH_DEBUG
VkDebugUtilsMessengerEXT mDebugMessenger = VK_NULL_HANDLE;
PFN_vkDestroyDebugUtilsMessengerEXT mVkDestroyDebugUtilsMessengerEXT = nullptr;
#endif
uint32 mGraphicsQueueIndex = 0;
uint32 mPresentQueueIndex = 0;
VkQueue mGraphicsQueue = VK_NULL_HANDLE;
VkQueue mPresentQueue = VK_NULL_HANDLE;
VkSurfaceFormatKHR mSelectedFormat;
PFN_vkGetInstanceProcAddr mVkGetInstanceProcAddr = nullptr;
PFN_vkGetDeviceProcAddr mVkGetDeviceProcAddr = nullptr;
// Pre instance functions
PFN_vkCreateInstance mVkCreateInstance = nullptr;
PFN_vkEnumerateInstanceExtensionProperties mVkEnumerateInstanceExtensionProperties = nullptr;
PFN_vkEnumerateInstanceLayerProperties mVkEnumerateInstanceLayerProperties = nullptr;
// Post instance functions
PFN_vkCreateDevice mVkCreateDevice = nullptr;
PFN_vkDestroyInstance mVkDestroyInstance = nullptr;
PFN_vkEnumerateDeviceExtensionProperties mVkEnumerateDeviceExtensionProperties = nullptr;
PFN_vkEnumeratePhysicalDevices mVkEnumeratePhysicalDevices = nullptr;
PFN_vkGetPhysicalDeviceProperties mVkGetPhysicalDeviceProperties = nullptr;
PFN_vkGetPhysicalDeviceQueueFamilyProperties mVkGetPhysicalDeviceQueueFamilyProperties = nullptr;
};
JPH_NAMESPACE_END
#endif // JPH_USE_VK

View file

@ -0,0 +1,181 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <Jolt/Jolt.h>
#ifdef JPH_USE_VK
#include <Jolt/Compute/VK/ComputeSystemVKWithAllocator.h>
#include <Jolt/Compute/VK/ComputeShaderVK.h>
#include <Jolt/Compute/VK/ComputeBufferVK.h>
#include <Jolt/Compute/VK/ComputeQueueVK.h>
JPH_NAMESPACE_BEGIN
JPH_IMPLEMENT_RTTI_VIRTUAL(ComputeSystemVKWithAllocator)
{
JPH_ADD_BASE_CLASS(ComputeSystemVKWithAllocator, ComputeSystemVK)
}
bool ComputeSystemVKWithAllocator::Initialize(VkInstance inInstance, VkPhysicalDevice inPhysicalDevice, PFN_vkGetInstanceProcAddr inGetInstanceProcAddr, PFN_vkGetDeviceProcAddr inVkGetDeviceProcAddr, VkDevice inDevice, uint32 inComputeQueueIndex, ComputeSystemResult &outResult)
{
#define JPH_LOAD_VK_INST(name) mVk##name = reinterpret_cast<PFN_vk##name>(reinterpret_cast<void *>(inGetInstanceProcAddr(inInstance, "vk" #name))); JPH_ASSERT(mVk##name != nullptr)
JPH_LOAD_VK_INST(GetPhysicalDeviceMemoryProperties);
#undef JPH_LOAD_VK_INST
return ComputeSystemVK::Initialize(inPhysicalDevice, inVkGetDeviceProcAddr, inDevice, inComputeQueueIndex, outResult);
}
bool ComputeSystemVKWithAllocator::InitializeMemory()
{
// Get memory properties
mVkGetPhysicalDeviceMemoryProperties(mPhysicalDevice, &mMemoryProperties);
return true;
}
void ComputeSystemVKWithAllocator::ShutdownMemory()
{
// Free all memory
for (const MemoryCache::value_type &mc : mMemoryCache)
for (const Memory &m : mc.second)
if (m.mOffset == 0)
FreeMemory(*m.mMemory);
mMemoryCache.clear();
}
uint32 ComputeSystemVKWithAllocator::FindMemoryType(uint32 inTypeFilter, VkMemoryPropertyFlags inProperties) const
{
for (uint32 i = 0; i < mMemoryProperties.memoryTypeCount; i++)
if ((inTypeFilter & (1 << i))
&& (mMemoryProperties.memoryTypes[i].propertyFlags & inProperties) == inProperties)
return i;
JPH_ASSERT(false, "Failed to find memory type!");
return 0;
}
void ComputeSystemVKWithAllocator::AllocateMemory(VkDeviceSize inSize, uint32 inMemoryTypeBits, VkMemoryPropertyFlags inProperties, MemoryVK &ioMemory)
{
JPH_ASSERT(ioMemory.mMemory == VK_NULL_HANDLE);
ioMemory.mSize = inSize;
ioMemory.mProperties = inProperties;
VkMemoryAllocateInfo alloc_info = {};
alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
alloc_info.allocationSize = inSize;
alloc_info.memoryTypeIndex = FindMemoryType(inMemoryTypeBits, inProperties);
mVkAllocateMemory(mDevice, &alloc_info, nullptr, &ioMemory.mMemory);
}
void ComputeSystemVKWithAllocator::FreeMemory(MemoryVK &ioMemory)
{
mVkFreeMemory(mDevice, ioMemory.mMemory, nullptr);
ioMemory.mMemory = VK_NULL_HANDLE;
}
bool ComputeSystemVKWithAllocator::CreateBuffer(VkDeviceSize inSize, VkBufferUsageFlags inUsage, VkMemoryPropertyFlags inProperties, BufferVK &outBuffer)
{
// Create a new buffer
outBuffer.mSize = inSize;
VkBufferCreateInfo create_info = {};
create_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
create_info.size = inSize;
create_info.usage = inUsage;
create_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
if (VKFailed(mVkCreateBuffer(mDevice, &create_info, nullptr, &outBuffer.mBuffer)))
{
outBuffer.mBuffer = VK_NULL_HANDLE;
return false;
}
VkMemoryRequirements mem_requirements;
mVkGetBufferMemoryRequirements(mDevice, outBuffer.mBuffer, &mem_requirements);
if (mem_requirements.size > cMaxAllocSize)
{
// Allocate block directly
Ref<MemoryVK> memory_vk = new MemoryVK();
memory_vk->mBufferSize = mem_requirements.size;
AllocateMemory(mem_requirements.size, mem_requirements.memoryTypeBits, inProperties, *memory_vk);
outBuffer.mMemory = memory_vk;
outBuffer.mOffset = 0;
}
else
{
// Round allocation to the next power of 2 so that we can use a simple block based allocator
VkDeviceSize buffer_size = max(VkDeviceSize(GetNextPowerOf2(uint32(mem_requirements.size))), cMinAllocSize);
// Ensure that we have memory available from the right pool
Array<Memory> &mem_array = mMemoryCache[{ buffer_size, inProperties }];
if (mem_array.empty())
{
// Allocate a bigger block
Ref<MemoryVK> memory_vk = new MemoryVK();
memory_vk->mBufferSize = buffer_size;
AllocateMemory(cBlockSize, mem_requirements.memoryTypeBits, inProperties, *memory_vk);
// Divide into sub blocks
for (VkDeviceSize offset = 0; offset < cBlockSize; offset += buffer_size)
mem_array.push_back({ memory_vk, offset });
}
// Claim memory from the pool
Memory &memory = mem_array.back();
outBuffer.mMemory = memory.mMemory;
outBuffer.mOffset = memory.mOffset;
mem_array.pop_back();
}
// Bind the memory to the buffer
mVkBindBufferMemory(mDevice, outBuffer.mBuffer, outBuffer.mMemory->mMemory, outBuffer.mOffset);
return true;
}
void ComputeSystemVKWithAllocator::FreeBuffer(BufferVK &ioBuffer)
{
if (ioBuffer.mBuffer != VK_NULL_HANDLE)
{
// Destroy the buffer
mVkDestroyBuffer(mDevice, ioBuffer.mBuffer, nullptr);
ioBuffer.mBuffer = VK_NULL_HANDLE;
// Hand the memory back to the cache
VkDeviceSize buffer_size = ioBuffer.mMemory->mBufferSize;
if (buffer_size > cMaxAllocSize)
FreeMemory(*ioBuffer.mMemory);
else
mMemoryCache[{ buffer_size, ioBuffer.mMemory->mProperties }].push_back({ ioBuffer.mMemory, ioBuffer.mOffset });
ioBuffer = BufferVK();
}
}
void *ComputeSystemVKWithAllocator::MapBuffer(BufferVK& ioBuffer)
{
if (++ioBuffer.mMemory->mMappedCount == 1
&& VKFailed(mVkMapMemory(mDevice, ioBuffer.mMemory->mMemory, 0, VK_WHOLE_SIZE, 0, &ioBuffer.mMemory->mMappedPtr)))
{
ioBuffer.mMemory->mMappedCount = 0;
return nullptr;
}
return static_cast<uint8 *>(ioBuffer.mMemory->mMappedPtr) + ioBuffer.mOffset;
}
void ComputeSystemVKWithAllocator::UnmapBuffer(BufferVK& ioBuffer)
{
JPH_ASSERT(ioBuffer.mMemory->mMappedCount > 0);
if (--ioBuffer.mMemory->mMappedCount == 0)
{
mVkUnmapMemory(mDevice, ioBuffer.mMemory->mMemory);
ioBuffer.mMemory->mMappedPtr = nullptr;
}
}
JPH_NAMESPACE_END
#endif // JPH_USE_VK

View file

@ -0,0 +1,75 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#ifdef JPH_USE_VK
#include <Jolt/Compute/VK/ComputeSystemVK.h>
#include <Jolt/Core/UnorderedMap.h>
JPH_NAMESPACE_BEGIN
/// This extends ComputeSystemVK to provide a default implementation for memory allocation and mapping.
/// It uses a simple block based allocator to reduce the number of allocations done to Vulkan.
class JPH_EXPORT ComputeSystemVKWithAllocator : public ComputeSystemVK
{
public:
JPH_DECLARE_RTTI_VIRTUAL(JPH_EXPORT, ComputeSystemVKWithAllocator)
// Initialize the compute system
bool Initialize(VkInstance inInstance, VkPhysicalDevice inPhysicalDevice, PFN_vkGetInstanceProcAddr inGetInstanceProcAddr, PFN_vkGetDeviceProcAddr inVkGetDeviceProcAddr, VkDevice inDevice, uint32 inComputeQueueIndex, ComputeSystemResult &outResult);
/// Allow the application to override buffer creation and memory mapping in case it uses its own allocator
virtual bool CreateBuffer(VkDeviceSize inSize, VkBufferUsageFlags inUsage, VkMemoryPropertyFlags inProperties, BufferVK &outBuffer) override;
virtual void FreeBuffer(BufferVK &ioBuffer) override;
virtual void * MapBuffer(BufferVK &ioBuffer) override;
virtual void UnmapBuffer(BufferVK &ioBuffer) override;
protected:
virtual bool InitializeMemory() override;
virtual void ShutdownMemory() override;
uint32 FindMemoryType(uint32 inTypeFilter, VkMemoryPropertyFlags inProperties) const;
void AllocateMemory(VkDeviceSize inSize, uint32 inMemoryTypeBits, VkMemoryPropertyFlags inProperties, MemoryVK &ioMemory);
void FreeMemory(MemoryVK &ioMemory);
VkPhysicalDeviceMemoryProperties mMemoryProperties;
PFN_vkGetPhysicalDeviceMemoryProperties mVkGetPhysicalDeviceMemoryProperties = nullptr;
private:
// Smaller allocations (from cMinAllocSize to cMaxAllocSize) will be done in blocks of cBlockSize bytes.
// We do this because there is a limit to the number of allocations that we can make in Vulkan.
static constexpr VkDeviceSize cMinAllocSize = 512;
static constexpr VkDeviceSize cMaxAllocSize = 65536;
static constexpr VkDeviceSize cBlockSize = 524288;
struct MemoryKey
{
bool operator == (const MemoryKey &inRHS) const
{
return mSize == inRHS.mSize && mProperties == inRHS.mProperties;
}
VkDeviceSize mSize;
VkMemoryPropertyFlags mProperties;
};
JPH_MAKE_HASH_STRUCT(MemoryKey, MemoryKeyHasher, t.mProperties, t.mSize)
struct Memory
{
Ref<MemoryVK> mMemory;
VkDeviceSize mOffset;
};
using MemoryCache = UnorderedMap<MemoryKey, Array<Memory>, MemoryKeyHasher>;
MemoryCache mMemoryCache;
};
JPH_NAMESPACE_END
#endif // JPH_USE_VK

View file

@ -0,0 +1,43 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/Core/StringTools.h>
#ifdef JPH_USE_VK
JPH_SUPPRESS_WARNINGS_STD_BEGIN
JPH_CLANG_SUPPRESS_WARNING("-Wc++98-compat-pedantic")
#include <vulkan/vulkan.h>
JPH_SUPPRESS_WARNINGS_STD_END
JPH_NAMESPACE_BEGIN
inline bool VKFailed(VkResult inResult)
{
if (inResult == VK_SUCCESS)
return false;
Trace("Vulkan call failed with error code: %d", (int)inResult);
JPH_ASSERT(false);
return true;
}
template <class Result>
inline bool VKFailed(VkResult inResult, Result &outResult)
{
if (inResult == VK_SUCCESS)
return false;
String error = StringFormat("Vulkan call failed with error code: %d", (int)inResult);
outResult.SetError(error);
return true;
}
JPH_NAMESPACE_END
#endif // JPH_USE_VK

View file

@ -0,0 +1,118 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2023 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
JPH_NAMESPACE_BEGIN
/// Construct a string that lists the most important configuration settings
inline const char *GetConfigurationString()
{
return JPH_IF_SINGLE_PRECISION_ELSE("Single", "Double") " precision "
#if defined(JPH_CPU_X86)
"x86 "
#elif defined(JPH_CPU_ARM)
"ARM "
#elif defined(JPH_CPU_RISCV)
"RISC-V "
#elif defined(JPH_CPU_PPC)
"PowerPC "
#ifdef JPH_CPU_BIG_ENDIAN
"(Big Endian) "
#else
"(Little Endian) "
#endif
#elif defined(JPH_CPU_LOONGARCH)
"LoongArch "
#elif defined(JPH_CPU_E2K)
"E2K "
#elif defined(JPH_CPU_WASM)
"WASM "
#else
#error Unknown CPU architecture
#endif
#if JPH_CPU_ARCH_BITS == 64
"64-bit "
#elif JPH_CPU_ARCH_BITS == 32
"32-bit "
#endif
"with instructions: "
#ifdef JPH_USE_RVV
"RVV "
#endif
#ifdef JPH_USE_NEON
"NEON "
#endif
#ifdef JPH_USE_SSE
"SSE2 "
#endif
#ifdef JPH_USE_SSE4_1
"SSE4.1 "
#endif
#ifdef JPH_USE_SSE4_2
"SSE4.2 "
#endif
#ifdef JPH_USE_AVX
"AVX "
#endif
#ifdef JPH_USE_AVX2
"AVX2 "
#endif
#ifdef JPH_USE_AVX512
"AVX512 "
#endif
#ifdef JPH_USE_F16C
"F16C "
#endif
#ifdef JPH_USE_LZCNT
"LZCNT "
#endif
#ifdef JPH_USE_TZCNT
"TZCNT "
#endif
#ifdef JPH_USE_FMADD
"FMADD "
#endif
#ifdef JPH_CROSS_PLATFORM_DETERMINISTIC
"(Cross Platform Deterministic) "
#endif
#ifdef JPH_FLOATING_POINT_EXCEPTIONS_ENABLED
"(FP Exceptions) "
#endif
#ifdef JPH_DEBUG_RENDERER
"(Debug Renderer) "
#endif
#ifdef JPH_PROFILE_ENABLED
"(Profile) "
#endif
#ifdef JPH_EXTERNAL_PROFILE
"(External Profile) "
#endif
#if defined(JPH_OBJECT_LAYER_BITS) && JPH_OBJECT_LAYER_BITS == 32
"(32-bit ObjectLayer) "
#else
"(16-bit ObjectLayer) "
#endif
#ifdef JPH_ENABLE_ASSERTS
"(Assertions) "
#endif
#ifdef JPH_OBJECT_STREAM
"(ObjectStream) "
#endif
#ifdef JPH_DEBUG
"(Debug) "
#endif
#if defined(__cpp_rtti) && __cpp_rtti
"(C++ RTTI) "
#endif
#if defined(__cpp_exceptions) && __cpp_exceptions
"(C++ Exceptions) "
#endif
#ifdef JPH_SHARED_LIBRARY
"(Shared Library) "
#endif
;
}
JPH_NAMESPACE_END

View file

@ -0,0 +1,340 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2022 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#ifdef JPH_USE_NEON
// Constructing NEON values
#ifdef JPH_COMPILER_MSVC
#define JPH_NEON_INT32x4(v1, v2, v3, v4) { int64_t(v1) + (int64_t(v2) << 32), int64_t(v3) + (int64_t(v4) << 32) }
#define JPH_NEON_UINT32x4(v1, v2, v3, v4) { uint64_t(v1) + (uint64_t(v2) << 32), uint64_t(v3) + (uint64_t(v4) << 32) }
#define JPH_NEON_INT8x16(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16) { int64_t(v1) + (int64_t(v2) << 8) + (int64_t(v3) << 16) + (int64_t(v4) << 24) + (int64_t(v5) << 32) + (int64_t(v6) << 40) + (int64_t(v7) << 48) + (int64_t(v8) << 56), int64_t(v9) + (int64_t(v10) << 8) + (int64_t(v11) << 16) + (int64_t(v12) << 24) + (int64_t(v13) << 32) + (int64_t(v14) << 40) + (int64_t(v15) << 48) + (int64_t(v16) << 56) }
#define JPH_NEON_UINT8x16(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16) { uint64_t(v1) + (uint64_t(v2) << 8) + (uint64_t(v3) << 16) + (uint64_t(v4) << 24) + (uint64_t(v5) << 32) + (uint64_t(v6) << 40) + (uint64_t(v7) << 48) + (uint64_t(v8) << 56), uint64_t(v9) + (uint64_t(v10) << 8) + (uint64_t(v11) << 16) + (uint64_t(v12) << 24) + (uint64_t(v13) << 32) + (uint64_t(v14) << 40) + (uint64_t(v15) << 48) + (uint64_t(v16) << 56) }
#else
#define JPH_NEON_INT32x4(v1, v2, v3, v4) { v1, v2, v3, v4 }
#define JPH_NEON_UINT32x4(v1, v2, v3, v4) { v1, v2, v3, v4 }
#define JPH_NEON_INT8x16(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16) { v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16 }
#define JPH_NEON_UINT8x16(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16) { v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16 }
#endif
// MSVC and GCC prior to version 12 don't define __builtin_shufflevector
#if defined(JPH_COMPILER_MSVC) || (defined(JPH_COMPILER_GCC) && __GNUC__ < 12)
JPH_NAMESPACE_BEGIN
// Generic shuffle vector template
template <unsigned I1, unsigned I2, unsigned I3, unsigned I4>
JPH_INLINE float32x4_t NeonShuffleFloat32x4(float32x4_t inV1, float32x4_t inV2)
{
float32x2_t lo = vcopy_laneq_f32(vdup_n_f32(0), 0, I1 >= 4? inV2 : inV1, I1 & 0b11);
lo = vcopy_laneq_f32(lo, 1, I2 >= 4? inV2 : inV1, I2 & 0b11);
float32x2_t hi = vcopy_laneq_f32(vdup_n_f32(0), 0, I3 >= 4? inV2 : inV1, I3 & 0b11);
hi = vcopy_laneq_f32(hi, 1, I4 >= 4? inV2 : inV1, I4 & 0b11);
return vcombine_f32(lo, hi);
}
// Specializations
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<0, 0, 0, 0>(float32x4_t inV1, float32x4_t inV2)
{
return vdupq_laneq_f32(inV1, 0);
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<0, 1, 0, 0>(float32x4_t inV1, float32x4_t inV2)
{
return vcombine_f32(vget_low_f32(inV1), vdup_lane_f32(vget_low_f32(inV1), 0));
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<0, 1, 2, 2>(float32x4_t inV1, float32x4_t inV2)
{
return vcopyq_laneq_f32(inV1, 3, inV1, 2);
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<0, 1, 2, 3>(float32x4_t inV1, float32x4_t inV2)
{
return inV1;
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<0, 1, 3, 2>(float32x4_t inV1, float32x4_t inV2)
{
return vcombine_f32(vget_low_f32(inV1), vrev64_f32(vget_high_f32(inV1)));
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<0, 1, 3, 3>(float32x4_t inV1, float32x4_t inV2)
{
return vcopyq_laneq_f32(inV1, 2, inV1, 3);
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<0, 1, 4, 5>(float32x4_t inV1, float32x4_t inV2)
{
return vreinterpretq_f32_f64(vzip1q_f64(vreinterpretq_f64_f32(inV1), vreinterpretq_f64_f32(inV2)));
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<0, 2, 1, 1>(float32x4_t inV1, float32x4_t inV2)
{
return vuzp1q_f32(inV1, vdupq_laneq_f32(inV1, 1));
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<0, 2, 1, 3>(float32x4_t inV1, float32x4_t inV2)
{
return vuzp1q_f32(inV1, vrev64q_f32(inV1));
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<0, 2, 2, 2>(float32x4_t inV1, float32x4_t inV2)
{
return vuzp1q_f32(inV1, vdupq_laneq_f32(inV1, 2));
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<0, 2, 2, 3>(float32x4_t inV1, float32x4_t inV2)
{
return vcopyq_laneq_f32(inV1, 1, inV1, 2);
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<0, 2, 3, 2>(float32x4_t inV1, float32x4_t inV2)
{
return vcopyq_laneq_f32(vuzp1q_f32(inV1, inV1), 2, inV1, 3);
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<0, 2, 3, 3>(float32x4_t inV1, float32x4_t inV2)
{
return vuzp1q_f32(inV1, vdupq_laneq_f32(inV1, 3));
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<0, 2, 4, 6>(float32x4_t inV1, float32x4_t inV2)
{
return vuzp1q_f32(inV1, inV2);
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<0, 3, 1, 2>(float32x4_t inV1, float32x4_t inV2)
{
return vzip1q_f32(inV1, vextq_f32(inV1, vdupq_laneq_f32(inV1, 2), 3));
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<1, 0, 0, 0>(float32x4_t inV1, float32x4_t inV2)
{
return vcombine_f32(vrev64_f32(vget_low_f32(inV1)), vdup_lane_f32(vget_low_f32(inV1), 0));
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<1, 0, 0, 2>(float32x4_t inV1, float32x4_t inV2)
{
return vcombine_f32(vrev64_f32(vget_low_f32(inV1)), vzip1_f32(vget_low_f32(inV1), vget_high_f32(inV1)));
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<1, 0, 3, 2>(float32x4_t inV1, float32x4_t inV2)
{
return vrev64q_f32(inV1);
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<1, 1, 1, 1>(float32x4_t inV1, float32x4_t inV2)
{
return vdupq_laneq_f32(inV1, 1);
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<1, 1, 2, 2>(float32x4_t inV1, float32x4_t inV2)
{
float32x4_t t = vextq_f32(inV1, inV1, 1);
return vzip1q_f32(t, t);
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<1, 1, 3, 3>(float32x4_t inV1, float32x4_t inV2)
{
return vtrn2q_f32(inV1, inV1);
}
// Used extensively by cross product
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<1, 2, 0, 0>(float32x4_t inV1, float32x4_t inV2)
{
return vcopyq_laneq_f32(vextq_f32(inV1, inV1, 1), 2, inV1, 0);
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<1, 2, 0, 1>(float32x4_t inV1, float32x4_t inV2)
{
return vextq_f32(vextq_f32(inV1, inV1, 3), inV1, 2);
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<1, 2, 0, 2>(float32x4_t inV1, float32x4_t inV2)
{
return vcopyq_laneq_f32(vuzp1q_f32(inV1, inV1), 0, inV1, 1);
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<1, 2, 2, 2>(float32x4_t inV1, float32x4_t inV2)
{
return vcombine_f32(vext_f32(vget_low_f32(inV1), vget_high_f32(inV1), 1), vdup_lane_f32(vget_high_f32(inV1), 0));
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<1, 2, 3, 2>(float32x4_t inV1, float32x4_t inV2)
{
return vextq_f32(inV1, vdupq_laneq_f32(inV1, 2), 1);
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<1, 2, 3, 3>(float32x4_t inV1, float32x4_t inV2)
{
return vextq_f32(inV1, vdupq_laneq_f32(inV1, 3), 1);
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<1, 3, 0, 2>(float32x4_t inV1, float32x4_t inV2)
{
return vuzp2q_f32(inV1, vrev64q_f32(inV1));
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<1, 3, 5, 7>(float32x4_t inV1, float32x4_t inV2)
{
return vuzp2q_f32(inV1, inV2);
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<2, 0, 1, 1>(float32x4_t inV1, float32x4_t inV2)
{
return vcopyq_laneq_f32(vzip1q_f32(inV1, inV1), 0, inV1, 2);
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<2, 0, 1, 2>(float32x4_t inV1, float32x4_t inV2)
{
return vextq_f32(vuzp1q_f32(inV1, inV1), inV1, 3);
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<2, 1, 0, 0>(float32x4_t inV1, float32x4_t inV2)
{
float32x4_t t = vextq_f32(vuzp1q_f32(inV1, inV1), inV1, 3);
return vuzp1q_f32(t, vuzp1q_f32(inV1, inV1));
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<2, 1, 0, 3>(float32x4_t inV1, float32x4_t inV2)
{
float32x4_t t = vrev64q_f32(inV1);
return vextq_f32(t, t, 3);
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<2, 2, 1, 0>(float32x4_t inV1, float32x4_t inV2)
{
return vextq_f32(vtrn1q_f32(inV1, inV1), vrev64q_f32(inV1), 2);
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<2, 2, 1, 1>(float32x4_t inV1, float32x4_t inV2)
{
float32x4_t t = vcopyq_laneq_f32(inV1, 3, inV1, 1);
return vzip2q_f32(t, t);
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<2, 2, 1, 2>(float32x4_t inV1, float32x4_t inV2)
{
return vcopyq_laneq_f32(vdupq_laneq_f32(inV1, 2), 2, inV1, 1);
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<2, 2, 2, 2>(float32x4_t inV1, float32x4_t inV2)
{
return vdupq_laneq_f32(inV1, 2);
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<2, 3, 0, 1>(float32x4_t inV1, float32x4_t inV2)
{
return vcombine_f32(vget_high_f32(inV1), vget_low_f32(inV1));
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<2, 3, 1, 2>(float32x4_t inV1, float32x4_t inV2)
{
return vextq_f32(inV1, vextq_f32(inV1, inV1, 1), 2);
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<2, 3, 2, 2>(float32x4_t inV1, float32x4_t inV2)
{
return vextq_f32(inV1, vdupq_laneq_f32(inV1, 2), 2);
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<2, 3, 2, 3>(float32x4_t inV1, float32x4_t inV2)
{
return vreinterpretq_f32_f64(vdupq_laneq_f64(vreinterpretq_f64_f32(inV1), 1));
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<2, 3, 6, 7>(float32x4_t inV1, float32x4_t inV2)
{
return vreinterpretq_f32_f64(vzip2q_f64(vreinterpretq_f64_f32(inV1), vreinterpretq_f64_f32(inV2)));
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<3, 0, 1, 2>(float32x4_t inV1, float32x4_t inV2)
{
return vextq_f32(inV1, inV1, 3);
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<3, 0, 3, 2>(float32x4_t inV1, float32x4_t inV2)
{
return vtrn1q_f32(vdupq_laneq_f32(inV1, 3), inV1);
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<3, 2, 1, 0>(float32x4_t inV1, float32x4_t inV2)
{
float32x4_t t = vrev64q_f32(inV1);
return vextq_f32(t, t, 2);
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<3, 2, 3, 2>(float32x4_t inV1, float32x4_t inV2)
{
float32x2_t zy = vrev64_f32(vget_high_f32(inV1));
return vcombine_f32(zy, zy);
}
template <>
JPH_INLINE float32x4_t NeonShuffleFloat32x4<3, 3, 3, 3>(float32x4_t inV1, float32x4_t inV2)
{
return vdupq_laneq_f32(inV1, 3);
}
// Shuffle a vector
#define JPH_NEON_SHUFFLE_F32x4(vec1, vec2, index1, index2, index3, index4) NeonShuffleFloat32x4<index1, index2, index3, index4>(vec1, vec2)
#define JPH_NEON_SHUFFLE_U32x4(vec1, vec2, index1, index2, index3, index4) vreinterpretq_u32_f32((NeonShuffleFloat32x4<index1, index2, index3, index4>(vreinterpretq_f32_u32(vec1), vreinterpretq_f32_u32(vec2))))
JPH_NAMESPACE_END
#else
// Shuffle a vector
#define JPH_NEON_SHUFFLE_F32x4(vec1, vec2, index1, index2, index3, index4) __builtin_shufflevector(vec1, vec2, index1, index2, index3, index4)
#define JPH_NEON_SHUFFLE_U32x4(vec1, vec2, index1, index2, index3, index4) __builtin_shufflevector(vec1, vec2, index1, index2, index3, index4)
#endif
#endif // JPH_USE_NEON

713
thirdparty/JoltPhysics/Jolt/Core/Array.h vendored Normal file
View file

@ -0,0 +1,713 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/Core/STLAllocator.h>
#include <Jolt/Core/HashCombine.h>
#ifdef JPH_USE_STD_VECTOR
JPH_SUPPRESS_WARNINGS_STD_BEGIN
#include <vector>
JPH_SUPPRESS_WARNINGS_STD_END
JPH_NAMESPACE_BEGIN
template <class T, class Allocator = STLAllocator<T>> using Array = std::vector<T, Allocator>;
JPH_NAMESPACE_END
#else
JPH_NAMESPACE_BEGIN
/// Simple replacement for std::vector
///
/// Major differences:
/// - Memory is not initialized to zero (this was causing a lot of page faults when deserializing large MeshShapes / HeightFieldShapes)
/// - Iterators are simple pointers (for now)
/// - No exception safety
/// - No specialization like std::vector<bool> has
/// - Not all functions have been implemented
template <class T, class Allocator = STLAllocator<T>>
class [[nodiscard]] Array : private Allocator
{
public:
using value_type = T;
using allocator_type = Allocator;
using size_type = size_t;
using difference_type = typename Allocator::difference_type;
using pointer = T *;
using const_pointer = const T *;
using reference = T &;
using const_reference = const T &;
using const_iterator = const T *;
using iterator = T *;
/// An iterator that traverses the array in reverse order
class rev_it
{
public:
/// Constructor
rev_it() = default;
explicit rev_it(T *inValue) : mValue(inValue) { }
/// Copying
rev_it(const rev_it &) = default;
rev_it & operator = (const rev_it &) = default;
/// Comparison
bool operator == (const rev_it &inRHS) const { return mValue == inRHS.mValue; }
bool operator != (const rev_it &inRHS) const { return mValue != inRHS.mValue; }
/// Arithmetics
rev_it & operator ++ () { --mValue; return *this; }
rev_it operator ++ (int) { return rev_it(mValue--); }
rev_it & operator -- () { ++mValue; return *this; }
rev_it operator -- (int) { return rev_it(mValue++); }
rev_it operator + (int inValue) const { return rev_it(mValue - inValue); }
rev_it operator - (int inValue) const { return rev_it(mValue + inValue); }
rev_it & operator += (int inValue) { mValue -= inValue; return *this; }
rev_it & operator -= (int inValue) { mValue += inValue; return *this; }
/// Access
T & operator * () const { return *mValue; }
T & operator -> () const { return *mValue; }
private:
T * mValue;
};
/// A const iterator that traverses the array in reverse order
class crev_it
{
public:
/// Constructor
crev_it() = default;
explicit crev_it(const T *inValue) : mValue(inValue) { }
/// Copying
crev_it(const crev_it &) = default;
explicit crev_it(const rev_it &inValue) : mValue(inValue.mValue) { }
crev_it & operator = (const crev_it &) = default;
crev_it & operator = (const rev_it &inRHS) { mValue = inRHS.mValue; return *this; }
/// Comparison
bool operator == (const crev_it &inRHS) const { return mValue == inRHS.mValue; }
bool operator != (const crev_it &inRHS) const { return mValue != inRHS.mValue; }
/// Arithmetics
crev_it & operator ++ () { --mValue; return *this; }
crev_it operator ++ (int) { return crev_it(mValue--); }
crev_it & operator -- () { ++mValue; return *this; }
crev_it operator -- (int) { return crev_it(mValue++); }
crev_it operator + (int inValue) { return crev_it(mValue - inValue); }
crev_it operator - (int inValue) { return crev_it(mValue + inValue); }
crev_it & operator += (int inValue) { mValue -= inValue; return *this; }
crev_it & operator -= (int inValue) { mValue += inValue; return *this; }
/// Access
const T & operator * () const { return *mValue; }
const T & operator -> () const { return *mValue; }
private:
const T * mValue;
};
using reverse_iterator = rev_it;
using const_reverse_iterator = crev_it;
private:
/// Move elements from one location to another
inline void move(pointer inDestination, pointer inSource, size_type inCount)
{
if constexpr (std::is_trivially_copyable<T>())
memmove(inDestination, inSource, inCount * sizeof(T));
else
{
if (inDestination < inSource)
{
for (T *destination_end = inDestination + inCount; inDestination < destination_end; ++inDestination, ++inSource)
{
new (inDestination) T(std::move(*inSource));
inSource->~T();
}
}
else
{
for (T *destination = inDestination + inCount - 1, *source = inSource + inCount - 1; destination >= inDestination; --destination, --source)
{
new (destination) T(std::move(*source));
source->~T();
}
}
}
}
/// Reallocate the data block to inNewCapacity
inline void reallocate(size_type inNewCapacity)
{
JPH_ASSERT(inNewCapacity > 0 && inNewCapacity >= mSize);
pointer ptr;
if constexpr (AllocatorHasReallocate<Allocator>::sValue)
{
// Reallocate data block
ptr = get_allocator().reallocate(mElements, mCapacity, inNewCapacity);
}
else
{
// Copy data to a new location
ptr = get_allocator().allocate(inNewCapacity);
if (mElements != nullptr)
{
move(ptr, mElements, mSize);
get_allocator().deallocate(mElements, mCapacity);
}
}
mElements = ptr;
mCapacity = inNewCapacity;
}
/// Destruct elements [inStart, inEnd - 1]
inline void destruct(size_type inStart, size_type inEnd)
{
if constexpr (!std::is_trivially_destructible<T>())
if (inStart < inEnd)
for (T *element = mElements + inStart, *element_end = mElements + inEnd; element < element_end; ++element)
element->~T();
}
public:
/// Reserve array space
inline void reserve(size_type inNewSize)
{
if (mCapacity < inNewSize)
reallocate(inNewSize);
}
/// Resize array to new length
inline void resize(size_type inNewSize)
{
destruct(inNewSize, mSize);
reserve(inNewSize);
if constexpr (!std::is_trivially_constructible<T>())
for (T *element = mElements + mSize, *element_end = mElements + inNewSize; element < element_end; ++element)
new (element) T;
mSize = inNewSize;
}
/// Resize array to new length and initialize all elements with inValue
inline void resize(size_type inNewSize, const T &inValue)
{
JPH_ASSERT(&inValue < mElements || &inValue >= mElements + mSize, "Can't pass an element from the array to resize");
destruct(inNewSize, mSize);
reserve(inNewSize);
for (T *element = mElements + mSize, *element_end = mElements + inNewSize; element < element_end; ++element)
new (element) T(inValue);
mSize = inNewSize;
}
/// Destruct all elements and set length to zero
inline void clear()
{
destruct(0, mSize);
mSize = 0;
}
private:
/// Grow the array by at least inAmount elements
inline void grow(size_type inAmount = 1)
{
size_type min_size = mSize + inAmount;
if (min_size > mCapacity)
{
size_type new_capacity = max(min_size, mCapacity * 2);
reserve(new_capacity);
}
}
/// Free memory
inline void deallocate()
{
get_allocator().deallocate(mElements, mCapacity);
mElements = nullptr;
mCapacity = 0;
}
/// Destroy all elements and free memory
inline void destroy()
{
if (mElements != nullptr)
{
clear();
deallocate();
}
}
public:
/// Replace the contents of this array with inBegin .. inEnd
template <class Iterator>
inline void assign(Iterator inBegin, Iterator inEnd)
{
clear();
reserve(size_type(std::distance(inBegin, inEnd)));
for (Iterator element = inBegin; element != inEnd; ++element)
new (&mElements[mSize++]) T(*element);
}
/// Replace the contents of this array with inList
inline void assign(std::initializer_list<T> inList)
{
clear();
reserve(size_type(inList.size()));
for (const T &v : inList)
new (&mElements[mSize++]) T(v);
}
/// Default constructor
Array() = default;
/// Constructor with allocator
explicit inline Array(const Allocator &inAllocator) :
Allocator(inAllocator)
{
}
/// Constructor with length
explicit inline Array(size_type inLength, const Allocator &inAllocator = { }) :
Allocator(inAllocator)
{
resize(inLength);
}
/// Constructor with length and value
inline Array(size_type inLength, const T &inValue, const Allocator &inAllocator = { }) :
Allocator(inAllocator)
{
resize(inLength, inValue);
}
/// Constructor from initializer list
inline Array(std::initializer_list<T> inList, const Allocator &inAllocator = { }) :
Allocator(inAllocator)
{
assign(inList);
}
/// Constructor from iterator
inline Array(const_iterator inBegin, const_iterator inEnd, const Allocator &inAllocator = { }) :
Allocator(inAllocator)
{
assign(inBegin, inEnd);
}
/// Copy constructor
inline Array(const Array<T, Allocator> &inRHS) :
Allocator(inRHS.get_allocator())
{
assign(inRHS.begin(), inRHS.end());
}
/// Move constructor
inline Array(Array<T, Allocator> &&inRHS) noexcept :
Allocator(std::move(inRHS.get_allocator())),
mSize(inRHS.mSize),
mCapacity(inRHS.mCapacity),
mElements(inRHS.mElements)
{
inRHS.mSize = 0;
inRHS.mCapacity = 0;
inRHS.mElements = nullptr;
}
/// Destruct all elements
inline ~Array()
{
destroy();
}
/// Get the allocator
inline Allocator & get_allocator()
{
return *this;
}
inline const Allocator &get_allocator() const
{
return *this;
}
/// Add element to the back of the array
inline void push_back(const T &inValue)
{
JPH_ASSERT(&inValue < mElements || &inValue >= mElements + mSize, "Can't pass an element from the array to push_back");
grow();
T *element = mElements + mSize++;
new (element) T(inValue);
}
inline void push_back(T &&inValue)
{
grow();
T *element = mElements + mSize++;
new (element) T(std::move(inValue));
}
/// Construct element at the back of the array
template <class... A>
inline T & emplace_back(A &&... inValue)
{
grow();
T *element = mElements + mSize++;
new (element) T(std::forward<A>(inValue)...);
return *element;
}
/// Remove element from the back of the array
inline void pop_back()
{
JPH_ASSERT(mSize > 0);
mElements[--mSize].~T();
}
/// Returns true if there are no elements in the array
inline bool empty() const
{
return mSize == 0;
}
/// Returns amount of elements in the array
inline size_type size() const
{
return mSize;
}
/// Returns maximum amount of elements the array can hold
inline size_type capacity() const
{
return mCapacity;
}
/// Reduce the capacity of the array to match its size
void shrink_to_fit()
{
if (mElements != nullptr)
{
if (mSize == 0)
deallocate();
else if (mCapacity > mSize)
reallocate(mSize);
}
}
/// Swap the contents of two arrays
void swap(Array<T, Allocator> &inRHS) noexcept
{
std::swap(get_allocator(), inRHS.get_allocator());
std::swap(mSize, inRHS.mSize);
std::swap(mCapacity, inRHS.mCapacity);
std::swap(mElements, inRHS.mElements);
}
template <class Iterator>
void insert(const_iterator inPos, Iterator inBegin, Iterator inEnd)
{
size_type num_elements = size_type(std::distance(inBegin, inEnd));
if (num_elements > 0)
{
// After grow() inPos may be invalid
size_type first_element = inPos - mElements;
grow(num_elements);
T *element_begin = mElements + first_element;
T *element_end = element_begin + num_elements;
move(element_end, element_begin, mSize - first_element);
for (T *element = element_begin; element < element_end; ++element, ++inBegin)
new (element) T(*inBegin);
mSize += num_elements;
}
}
void insert(const_iterator inPos, const T &inValue)
{
JPH_ASSERT(&inValue < mElements || &inValue >= mElements + mSize, "Can't pass an element from the array to insert");
// After grow() inPos may be invalid
size_type first_element = inPos - mElements;
grow();
T *element = mElements + first_element;
move(element + 1, element, mSize - first_element);
new (element) T(inValue);
mSize++;
}
/// Remove one element from the array
iterator erase(const_iterator inIter)
{
size_type p = size_type(inIter - begin());
JPH_ASSERT(p < mSize);
mElements[p].~T();
if (p + 1 < mSize)
move(mElements + p, mElements + p + 1, mSize - p - 1);
--mSize;
return const_cast<iterator>(inIter);
}
/// Remove multiple element from the array
iterator erase(const_iterator inBegin, const_iterator inEnd)
{
size_type p = size_type(inBegin - begin());
size_type n = size_type(inEnd - inBegin);
JPH_ASSERT(inEnd <= end());
destruct(p, p + n);
if (p + n < mSize)
move(mElements + p, mElements + p + n, mSize - p - n);
mSize -= n;
return const_cast<iterator>(inBegin);
}
/// Iterators
inline const_iterator begin() const
{
return mElements;
}
inline const_iterator end() const
{
return mElements + mSize;
}
inline crev_it rbegin() const
{
return crev_it(mElements + mSize - 1);
}
inline crev_it rend() const
{
return crev_it(mElements - 1);
}
inline const_iterator cbegin() const
{
return begin();
}
inline const_iterator cend() const
{
return end();
}
inline crev_it crbegin() const
{
return rbegin();
}
inline crev_it crend() const
{
return rend();
}
inline iterator begin()
{
return mElements;
}
inline iterator end()
{
return mElements + mSize;
}
inline rev_it rbegin()
{
return rev_it(mElements + mSize - 1);
}
inline rev_it rend()
{
return rev_it(mElements - 1);
}
inline const T * data() const
{
return mElements;
}
inline T * data()
{
return mElements;
}
/// Access element
inline T & operator [] (size_type inIdx)
{
JPH_ASSERT(inIdx < mSize);
return mElements[inIdx];
}
inline const T & operator [] (size_type inIdx) const
{
JPH_ASSERT(inIdx < mSize);
return mElements[inIdx];
}
/// Access element
inline T & at(size_type inIdx)
{
JPH_ASSERT(inIdx < mSize);
return mElements[inIdx];
}
inline const T & at(size_type inIdx) const
{
JPH_ASSERT(inIdx < mSize);
return mElements[inIdx];
}
/// First element in the array
inline const T & front() const
{
JPH_ASSERT(mSize > 0);
return mElements[0];
}
inline T & front()
{
JPH_ASSERT(mSize > 0);
return mElements[0];
}
/// Last element in the array
inline const T & back() const
{
JPH_ASSERT(mSize > 0);
return mElements[mSize - 1];
}
inline T & back()
{
JPH_ASSERT(mSize > 0);
return mElements[mSize - 1];
}
/// Assignment operator
Array<T, Allocator> & operator = (const Array<T, Allocator> &inRHS)
{
if (static_cast<const void *>(this) != static_cast<const void *>(&inRHS))
assign(inRHS.begin(), inRHS.end());
return *this;
}
/// Assignment move operator
Array<T, Allocator> & operator = (Array<T, Allocator> &&inRHS) noexcept
{
if (static_cast<const void *>(this) != static_cast<const void *>(&inRHS))
{
destroy();
get_allocator() = std::move(inRHS.get_allocator());
mSize = inRHS.mSize;
mCapacity = inRHS.mCapacity;
mElements = inRHS.mElements;
inRHS.mSize = 0;
inRHS.mCapacity = 0;
inRHS.mElements = nullptr;
}
return *this;
}
/// Assignment operator
Array<T, Allocator> & operator = (std::initializer_list<T> inRHS)
{
assign(inRHS);
return *this;
}
/// Comparing arrays
bool operator == (const Array<T, Allocator> &inRHS) const
{
if (mSize != inRHS.mSize)
return false;
for (size_type i = 0; i < mSize; ++i)
if (!(mElements[i] == inRHS.mElements[i]))
return false;
return true;
}
bool operator != (const Array<T, Allocator> &inRHS) const
{
if (mSize != inRHS.mSize)
return true;
for (size_type i = 0; i < mSize; ++i)
if (mElements[i] != inRHS.mElements[i])
return true;
return false;
}
/// Get hash for this array
uint64 GetHash() const
{
// Hash length first
uint64 ret = Hash<uint32> { } (uint32(size()));
// Then hash elements
for (const T *element = mElements, *element_end = mElements + mSize; element < element_end; ++element)
HashCombine(ret, *element);
return ret;
}
private:
size_type mSize = 0;
size_type mCapacity = 0;
T * mElements = nullptr;
};
JPH_NAMESPACE_END
JPH_SUPPRESS_WARNING_PUSH
JPH_CLANG_SUPPRESS_WARNING("-Wc++98-compat")
namespace std
{
/// Declare std::hash for Array
template <class T, class Allocator>
struct hash<JPH::Array<T, Allocator>>
{
size_t operator () (const JPH::Array<T, Allocator> &inRHS) const
{
return std::size_t(inRHS.GetHash());
}
};
}
JPH_SUPPRESS_WARNING_POP
#endif // JPH_USE_STD_VECTOR

View file

@ -0,0 +1,44 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
JPH_SUPPRESS_WARNINGS_STD_BEGIN
#include <atomic>
JPH_SUPPRESS_WARNINGS_STD_END
JPH_NAMESPACE_BEGIN
// Things we're using from STL
using std::atomic;
using std::memory_order;
using std::memory_order_relaxed;
using std::memory_order_acquire;
using std::memory_order_release;
using std::memory_order_acq_rel;
using std::memory_order_seq_cst;
/// Atomically compute the min(ioAtomic, inValue) and store it in ioAtomic, returns true if value was updated
template <class T>
bool AtomicMin(atomic<T> &ioAtomic, const T inValue, const memory_order inMemoryOrder = memory_order_seq_cst)
{
T cur_value = ioAtomic.load(memory_order_relaxed);
while (cur_value > inValue)
if (ioAtomic.compare_exchange_weak(cur_value, inValue, inMemoryOrder))
return true;
return false;
}
/// Atomically compute the max(ioAtomic, inValue) and store it in ioAtomic, returns true if value was updated
template <class T>
bool AtomicMax(atomic<T> &ioAtomic, const T inValue, const memory_order inMemoryOrder = memory_order_seq_cst)
{
T cur_value = ioAtomic.load(memory_order_relaxed);
while (cur_value < inValue)
if (ioAtomic.compare_exchange_weak(cur_value, inValue, inMemoryOrder))
return true;
return false;
}
JPH_NAMESPACE_END

View file

@ -0,0 +1,96 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
JPH_NAMESPACE_BEGIN
/// Push a new element into a binary max-heap.
/// [inBegin, inEnd - 1) must be a a valid heap. Element inEnd - 1 will be inserted into the heap. The heap will be [inBegin, inEnd) after this call.
/// inPred is a function that returns true if the first element is less or equal than the second element.
/// See: https://en.wikipedia.org/wiki/Binary_heap
template <typename Iterator, typename Pred>
void BinaryHeapPush(Iterator inBegin, Iterator inEnd, Pred inPred)
{
using diff_t = typename std::iterator_traits<Iterator>::difference_type;
using elem_t = typename std::iterator_traits<Iterator>::value_type;
// New heap size
diff_t count = std::distance(inBegin, inEnd);
// Start from the last element
diff_t current = count - 1;
while (current > 0)
{
// Get current element
elem_t &current_elem = *(inBegin + current);
// Get parent element
diff_t parent = (current - 1) >> 1;
elem_t &parent_elem = *(inBegin + parent);
// Sort them so that the parent is larger than the child
if (inPred(parent_elem, current_elem))
{
std::swap(parent_elem, current_elem);
current = parent;
}
else
{
// When there's no change, we're done
break;
}
}
}
/// Pop an element from a binary max-heap.
/// [inBegin, inEnd) must be a valid heap. The largest element will be removed from the heap. The heap will be [inBegin, inEnd - 1) after this call.
/// inPred is a function that returns true if the first element is less or equal than the second element.
/// See: https://en.wikipedia.org/wiki/Binary_heap
template <typename Iterator, typename Pred>
void BinaryHeapPop(Iterator inBegin, Iterator inEnd, Pred inPred)
{
using diff_t = typename std::iterator_traits<Iterator>::difference_type;
// Begin by moving the highest element to the end, this is the popped element
std::swap(*(inEnd - 1), *inBegin);
// New heap size
diff_t count = std::distance(inBegin, inEnd) - 1;
// Start from the root
diff_t largest = 0;
for (;;)
{
// Get first child
diff_t child = (largest << 1) + 1;
// Check if we're beyond the end of the heap, if so the 2nd child is also beyond the end
if (child >= count)
break;
// Remember the largest element from the previous iteration
diff_t prev_largest = largest;
// Check if first child is bigger, if so select it
if (inPred(*(inBegin + largest), *(inBegin + child)))
largest = child;
// Switch to the second child
++child;
// Check if second child is bigger, if so select it
if (child < count && inPred(*(inBegin + largest), *(inBegin + child)))
largest = child;
// If there was no change, we're done
if (prev_largest == largest)
break;
// Swap element
std::swap(*(inBegin + prev_largest), *(inBegin + largest));
}
}
JPH_NAMESPACE_END

View file

@ -0,0 +1,74 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/Core/STLAlignedAllocator.h>
JPH_NAMESPACE_BEGIN
/// Underlying data type for ByteBuffer
using ByteBufferVector = Array<uint8, STLAlignedAllocator<uint8, JPH_CACHE_LINE_SIZE>>;
/// Simple byte buffer, aligned to a cache line
class ByteBuffer : public ByteBufferVector
{
public:
/// Align the size to a multiple of inSize, returns the length after alignment
size_t Align(size_t inSize)
{
// Assert power of 2
JPH_ASSERT(IsPowerOf2(inSize));
// Calculate new size and resize buffer
size_t s = AlignUp(size(), inSize);
resize(s, 0);
return s;
}
/// Allocate block of data of inSize elements and return the pointer
template <class Type>
Type * Allocate(size_t inSize = 1)
{
// Reserve space
size_t s = size();
resize(s + inSize * sizeof(Type));
// Get data pointer
Type *data = reinterpret_cast<Type *>(&at(s));
// Construct elements
for (Type *d = data, *d_end = data + inSize; d < d_end; ++d)
new (d) Type;
// Return pointer
return data;
}
/// Append inData to the buffer
template <class Type>
void AppendVector(const Array<Type> &inData)
{
size_t size = inData.size() * sizeof(Type);
uint8 *data = Allocate<uint8>(size);
memcpy(data, &inData[0], size);
}
/// Get object at inPosition (an offset in bytes)
template <class Type>
const Type * Get(size_t inPosition) const
{
return reinterpret_cast<const Type *>(&at(inPosition));
}
/// Get object at inPosition (an offset in bytes)
template <class Type>
Type * Get(size_t inPosition)
{
return reinterpret_cast<Type *>(&at(inPosition));
}
};
JPH_NAMESPACE_END

View file

@ -0,0 +1,38 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <Jolt/Jolt.h>
#include <Jolt/Core/Color.h>
JPH_NAMESPACE_BEGIN
// Predefined colors
const Color Color::sBlack(0, 0, 0);
const Color Color::sDarkRed(128, 0, 0);
const Color Color::sRed(255, 0, 0);
const Color Color::sDarkGreen(0, 128, 0);
const Color Color::sGreen(0, 255, 0);
const Color Color::sDarkBlue(0, 0, 128);
const Color Color::sBlue(0, 0, 255);
const Color Color::sYellow(255, 255, 0);
const Color Color::sPurple(255, 0, 255);
const Color Color::sCyan(0, 255, 255);
const Color Color::sOrange(255, 128, 0);
const Color Color::sDarkOrange(128, 64, 0);
const Color Color::sGrey(128, 128, 128);
const Color Color::sLightGrey(192, 192, 192);
const Color Color::sWhite(255, 255, 255);
// Generated by: http://phrogz.net/css/distinct-colors.html (this algo: https://en.wikipedia.org/wiki/Color_difference#CMC_l:c_.281984.29)
static constexpr Color sColors[] = { Color(255, 0, 0), Color(204, 143, 102), Color(226, 242, 0), Color(41, 166, 124), Color(0, 170, 255), Color(69, 38, 153), Color(153, 38, 130), Color(229, 57, 80), Color(204, 0, 0), Color(255, 170, 0), Color(85, 128, 0), Color(64, 255, 217), Color(0, 75, 140), Color(161, 115, 230), Color(242, 61, 157), Color(178, 101, 89), Color(140, 94, 0), Color(181, 217, 108), Color(64, 242, 255), Color(77, 117, 153), Color(157, 61, 242), Color(140, 0, 56), Color(127, 57, 32), Color(204, 173, 51), Color(64, 255, 64), Color(38, 145, 153), Color(0, 102, 255), Color(242, 0, 226), Color(153, 77, 107), Color(229, 92, 0), Color(140, 126, 70), Color(0, 179, 71), Color(0, 194, 242), Color(27, 0, 204), Color(230, 115, 222), Color(127, 0, 17) };
Color Color::sGetDistinctColor(int inIndex)
{
JPH_ASSERT(inIndex >= 0);
return sColors[inIndex % (sizeof(sColors) / sizeof(uint32))];
}
JPH_NAMESPACE_END

View file

@ -0,0 +1,98 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
JPH_NAMESPACE_BEGIN
class Color;
/// Type to use for passing arguments to a function
using ColorArg = Color;
/// Class that holds an RGBA color with 8-bits per component
class JPH_EXPORT_GCC_BUG_WORKAROUND [[nodiscard]] Color
{
public:
/// Constructors
Color() = default; ///< Intentionally not initialized for performance reasons
Color(const Color &inRHS) = default;
Color & operator = (const Color &inRHS) = default;
explicit constexpr Color(uint32 inColor) : mU32(inColor) { }
constexpr Color(uint8 inRed, uint8 inGreen, uint8 inBlue, uint8 inAlpha = 255) : r(inRed), g(inGreen), b(inBlue), a(inAlpha) { }
constexpr Color(ColorArg inRHS, uint8 inAlpha) : r(inRHS.r), g(inRHS.g), b(inRHS.b), a(inAlpha) { }
/// Comparison
inline bool operator == (ColorArg inRHS) const { return mU32 == inRHS.mU32; }
inline bool operator != (ColorArg inRHS) const { return mU32 != inRHS.mU32; }
/// Convert to uint32
uint32 GetUInt32() const { return mU32; }
/// Element access, 0 = red, 1 = green, 2 = blue, 3 = alpha
inline uint8 operator () (uint inIdx) const { JPH_ASSERT(inIdx < 4); return (&r)[inIdx]; }
inline uint8 & operator () (uint inIdx) { JPH_ASSERT(inIdx < 4); return (&r)[inIdx]; }
/// Multiply two colors
inline Color operator * (const Color &inRHS) const { return Color(uint8((uint32(r) * inRHS.r) >> 8), uint8((uint32(g) * inRHS.g) >> 8), uint8((uint32(b) * inRHS.b) >> 8), uint8((uint32(a) * inRHS.a) >> 8)); }
/// Multiply color with intensity in the range [0, 1]
inline Color operator * (float inIntensity) const { return Color(uint8(r * inIntensity), uint8(g * inIntensity), uint8(b * inIntensity), a); }
/// Convert to Vec4 with range [0, 1]
inline Vec4 ToVec4() const { return Vec4(r, g, b, a) / 255.0f; }
/// Get grayscale intensity of color
inline uint8 GetIntensity() const { return uint8((uint32(r) * 54 + g * 183 + b * 19) >> 8); }
/// Get a visually distinct color
static Color sGetDistinctColor(int inIndex);
/// Get a color value on the gradient from green through yellow to red
/// @param inValue Value in the range [0, 1], 0 = green, 0.5 = yellow, 1 = red
static Color sGreenRedGradient(float inValue)
{
if (inValue < 0.0f)
return Color::sGreen;
else if (inValue < 0.5f)
return Color(uint8(510.0f * inValue), 255, 0);
else if (inValue < 1.0f)
return Color(255, uint8(510.0f * (1.0f - inValue)), 0);
else
return Color::sRed;
}
/// Predefined colors
static const Color sBlack;
static const Color sDarkRed;
static const Color sRed;
static const Color sDarkGreen;
static const Color sGreen;
static const Color sDarkBlue;
static const Color sBlue;
static const Color sYellow;
static const Color sPurple;
static const Color sCyan;
static const Color sOrange;
static const Color sDarkOrange;
static const Color sGrey;
static const Color sLightGrey;
static const Color sWhite;
union
{
uint32 mU32; ///< Combined value for red, green, blue and alpha
struct
{
uint8 r; ///< Red channel
uint8 g; ///< Green channel
uint8 b; ///< Blue channel
uint8 a; ///< Alpha channel
};
};
};
static_assert(std::is_trivially_default_constructible<Color>() && std::is_trivially_copyable<Color>(), "Is supposed to be a trivial type!");
JPH_NAMESPACE_END

708
thirdparty/JoltPhysics/Jolt/Core/Core.h vendored Normal file
View file

@ -0,0 +1,708 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
// Jolt library version
#define JPH_VERSION_MAJOR 5
#define JPH_VERSION_MINOR 6
#define JPH_VERSION_PATCH 0
// Determine which features the library was compiled with
#ifdef JPH_DOUBLE_PRECISION
#define JPH_VERSION_FEATURE_BIT_1 1
#else
#define JPH_VERSION_FEATURE_BIT_1 0
#endif
#ifdef JPH_CROSS_PLATFORM_DETERMINISTIC
#define JPH_VERSION_FEATURE_BIT_2 1
#else
#define JPH_VERSION_FEATURE_BIT_2 0
#endif
#ifdef JPH_FLOATING_POINT_EXCEPTIONS_ENABLED
#define JPH_VERSION_FEATURE_BIT_3 1
#else
#define JPH_VERSION_FEATURE_BIT_3 0
#endif
#ifdef JPH_PROFILE_ENABLED
#define JPH_VERSION_FEATURE_BIT_4 1
#else
#define JPH_VERSION_FEATURE_BIT_4 0
#endif
#ifdef JPH_EXTERNAL_PROFILE
#define JPH_VERSION_FEATURE_BIT_5 1
#else
#define JPH_VERSION_FEATURE_BIT_5 0
#endif
#ifdef JPH_DEBUG_RENDERER
#define JPH_VERSION_FEATURE_BIT_6 1
#else
#define JPH_VERSION_FEATURE_BIT_6 0
#endif
#ifdef JPH_DISABLE_TEMP_ALLOCATOR
#define JPH_VERSION_FEATURE_BIT_7 1
#else
#define JPH_VERSION_FEATURE_BIT_7 0
#endif
#ifdef JPH_DISABLE_CUSTOM_ALLOCATOR
#define JPH_VERSION_FEATURE_BIT_8 1
#else
#define JPH_VERSION_FEATURE_BIT_8 0
#endif
#if defined(JPH_OBJECT_LAYER_BITS) && JPH_OBJECT_LAYER_BITS == 32
#define JPH_VERSION_FEATURE_BIT_9 1
#else
#define JPH_VERSION_FEATURE_BIT_9 0
#endif
#ifdef JPH_ENABLE_ASSERTS
#define JPH_VERSION_FEATURE_BIT_10 1
#else
#define JPH_VERSION_FEATURE_BIT_10 0
#endif
#ifdef JPH_OBJECT_STREAM
#define JPH_VERSION_FEATURE_BIT_11 1
#else
#define JPH_VERSION_FEATURE_BIT_11 0
#endif
#define JPH_VERSION_FEATURES (uint64(JPH_VERSION_FEATURE_BIT_1) | (JPH_VERSION_FEATURE_BIT_2 << 1) | (JPH_VERSION_FEATURE_BIT_3 << 2) | (JPH_VERSION_FEATURE_BIT_4 << 3) | (JPH_VERSION_FEATURE_BIT_5 << 4) | (JPH_VERSION_FEATURE_BIT_6 << 5) | (JPH_VERSION_FEATURE_BIT_7 << 6) | (JPH_VERSION_FEATURE_BIT_8 << 7) | (JPH_VERSION_FEATURE_BIT_9 << 8) | (JPH_VERSION_FEATURE_BIT_10 << 9) | (JPH_VERSION_FEATURE_BIT_11 << 10))
// Combine the version and features in a single ID
#define JPH_VERSION_ID ((JPH_VERSION_FEATURES << 24) | (JPH_VERSION_MAJOR << 16) | (JPH_VERSION_MINOR << 8) | JPH_VERSION_PATCH)
// Determine platform
#if defined(JPH_PLATFORM_BLUE)
// Correct define already defined, this overrides everything else
#elif defined(_WIN32) || defined(_WIN64)
#include <winapifamily.h>
#if WINAPI_FAMILY == WINAPI_FAMILY_APP
#define JPH_PLATFORM_WINDOWS_UWP // Building for Universal Windows Platform
#endif
#define JPH_PLATFORM_WINDOWS
#elif defined(__ANDROID__) // Android is linux too, so that's why we check it first
#define JPH_PLATFORM_ANDROID
#elif defined(__linux__)
#define JPH_PLATFORM_LINUX
#elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__)
#define JPH_PLATFORM_BSD
#elif defined(__APPLE__)
#include <TargetConditionals.h>
#if defined(TARGET_OS_IPHONE) && !TARGET_OS_IPHONE
#define JPH_PLATFORM_MACOS
#else
#define JPH_PLATFORM_IOS
#endif
#elif defined(__EMSCRIPTEN__)
#define JPH_PLATFORM_WASM
#endif
// Platform helper macros
#ifdef JPH_PLATFORM_ANDROID
#define JPH_IF_NOT_ANDROID(x)
#else
#define JPH_IF_NOT_ANDROID(x) x
#endif
// Determine compiler
#if defined(__clang__)
#define JPH_COMPILER_CLANG
#elif defined(__GNUC__)
#define JPH_COMPILER_GCC
#elif defined(_MSC_VER)
#define JPH_COMPILER_MSVC
#endif
#if defined(__MINGW64__) || defined (__MINGW32__)
#define JPH_COMPILER_MINGW
#endif
// Detect CPU architecture
#if defined(__aarch64__) || defined(_M_ARM64) || defined(__arm__) || defined(_M_ARM) || defined(_M_ARM64EC)
// ARM CPU architecture
#define JPH_CPU_ARM
#if defined(__aarch64__) || defined(_M_ARM64) || defined(_M_ARM64EC)
#define JPH_CPU_ARCH_BITS 64
#define JPH_USE_NEON
#define JPH_VECTOR_ALIGNMENT 16
#define JPH_DVECTOR_ALIGNMENT 32
#else
#define JPH_CPU_ARCH_BITS 32
#define JPH_VECTOR_ALIGNMENT 8 // 32-bit ARM does not support aligning on the stack on 16 byte boundaries
#define JPH_DVECTOR_ALIGNMENT 8
#endif
#ifndef JPH_CROSS_PLATFORM_DETERMINISTIC // FMA is not compatible with cross platform determinism
#if defined(__ARM_FEATURE_FMA) && !defined(JPH_USE_FMADD)
#define JPH_USE_FMADD
#endif
#endif
#elif defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86)
// X86 CPU architecture
#define JPH_CPU_X86
#if defined(__x86_64__) || defined(_M_X64)
#define JPH_CPU_ARCH_BITS 64
#else
#define JPH_CPU_ARCH_BITS 32
#endif
#define JPH_USE_SSE
#define JPH_VECTOR_ALIGNMENT 16
#define JPH_DVECTOR_ALIGNMENT 32
// Detect enabled instruction sets
#if defined(__AVX512F__) && defined(__AVX512VL__) && defined(__AVX512DQ__) && !defined(JPH_USE_AVX512)
#define JPH_USE_AVX512
#endif
#if (defined(__AVX2__) || defined(JPH_USE_AVX512)) && !defined(JPH_USE_AVX2)
#define JPH_USE_AVX2
#endif
#if (defined(__AVX__) || defined(JPH_USE_AVX2)) && !defined(JPH_USE_AVX)
#define JPH_USE_AVX
#endif
#if (defined(__SSE4_2__) || defined(JPH_USE_AVX)) && !defined(JPH_USE_SSE4_2)
#define JPH_USE_SSE4_2
#endif
#if (defined(__SSE4_1__) || defined(JPH_USE_SSE4_2)) && !defined(JPH_USE_SSE4_1)
#define JPH_USE_SSE4_1
#endif
#if (defined(__F16C__) || defined(JPH_USE_AVX2)) && !defined(JPH_USE_F16C)
#define JPH_USE_F16C
#endif
#if (defined(__LZCNT__) || defined(JPH_USE_AVX2)) && !defined(JPH_USE_LZCNT)
#define JPH_USE_LZCNT
#endif
#if (defined(__BMI__) || defined(JPH_USE_AVX2)) && !defined(JPH_USE_TZCNT)
#define JPH_USE_TZCNT
#endif
#ifndef JPH_CROSS_PLATFORM_DETERMINISTIC // FMA is not compatible with cross platform determinism
#if defined(JPH_COMPILER_CLANG) || defined(JPH_COMPILER_GCC)
#if defined(__FMA__) && !defined(JPH_USE_FMADD)
#define JPH_USE_FMADD
#endif
#elif defined(JPH_COMPILER_MSVC)
#if defined(__AVX2__) && !defined(JPH_USE_FMADD) // AVX2 also enables fused multiply add
#define JPH_USE_FMADD
#endif
#else
#error Undefined compiler
#endif
#endif
#elif defined(__riscv)
// RISC-V CPU architecture
#define JPH_CPU_RISCV
#if __riscv_xlen == 64
#define JPH_CPU_ARCH_BITS 64
#define JPH_VECTOR_ALIGNMENT 16
#define JPH_DVECTOR_ALIGNMENT 32
#else
#define JPH_CPU_ARCH_BITS 32
#define JPH_VECTOR_ALIGNMENT 16
#define JPH_DVECTOR_ALIGNMENT 8
#endif
#if defined(__riscv_vector)
#define JPH_USE_RVV
#endif
#elif defined(JPH_PLATFORM_WASM)
// WebAssembly CPU architecture
#define JPH_CPU_WASM
#if defined(__wasm64__)
#define JPH_CPU_ARCH_BITS 64
#else
#define JPH_CPU_ARCH_BITS 32
#endif
#define JPH_VECTOR_ALIGNMENT 16
#define JPH_DVECTOR_ALIGNMENT 32
#ifdef __wasm_simd128__
#define JPH_USE_SSE
#define JPH_USE_SSE4_1
#define JPH_USE_SSE4_2
#endif
#elif defined(__powerpc__) || defined(__powerpc64__)
// PowerPC CPU architecture
#define JPH_CPU_PPC
#if defined(__powerpc64__)
#define JPH_CPU_ARCH_BITS 64
#else
#define JPH_CPU_ARCH_BITS 32
#endif
#ifdef _BIG_ENDIAN
#define JPH_CPU_BIG_ENDIAN
#endif
#define JPH_VECTOR_ALIGNMENT 16
#define JPH_DVECTOR_ALIGNMENT 8
#elif defined(__loongarch__)
// LoongArch CPU architecture
#define JPH_CPU_LOONGARCH
#if defined(__loongarch64)
#define JPH_CPU_ARCH_BITS 64
#else
#define JPH_CPU_ARCH_BITS 32
#endif
#define JPH_VECTOR_ALIGNMENT 16
#define JPH_DVECTOR_ALIGNMENT 8
#elif defined(__e2k__)
// E2K CPU architecture (MCST Elbrus 2000)
#define JPH_CPU_E2K
#define JPH_CPU_ARCH_BITS 64
#define JPH_VECTOR_ALIGNMENT 16
#define JPH_DVECTOR_ALIGNMENT 32
// Compiler flags on e2k arch determine CPU features
#if defined(__SSE__) && !defined(JPH_USE_SSE)
#define JPH_USE_SSE
#endif
#else
#error Unsupported CPU architecture
#endif
// If this define is set, Jolt is compiled as a shared library
#ifdef JPH_SHARED_LIBRARY
#ifdef JPH_BUILD_SHARED_LIBRARY
// While building the shared library, we must export these symbols
#if defined(JPH_PLATFORM_WINDOWS) && !defined(JPH_COMPILER_MINGW)
#define JPH_EXPORT __declspec(dllexport)
#else
#define JPH_EXPORT __attribute__ ((visibility ("default")))
#if defined(JPH_COMPILER_GCC)
// Prevents an issue with GCC attribute parsing (see https://gcc.gnu.org/bugzilla/show_bug.cgi?id=69585)
#define JPH_EXPORT_GCC_BUG_WORKAROUND [[gnu::visibility("default")]]
#endif
#endif
#else
// When linking against Jolt, we must import these symbols
#if defined(JPH_PLATFORM_WINDOWS) && !defined(JPH_COMPILER_MINGW)
#define JPH_EXPORT __declspec(dllimport)
#else
#define JPH_EXPORT __attribute__ ((visibility ("default")))
#if defined(JPH_COMPILER_GCC)
// Prevents an issue with GCC attribute parsing (see https://gcc.gnu.org/bugzilla/show_bug.cgi?id=69585)
#define JPH_EXPORT_GCC_BUG_WORKAROUND [[gnu::visibility("default")]]
#endif
#endif
#endif
#define JPH_IF_SHARED_LIBRARY(x) x
#define JPH_IF_NOT_SHARED_LIBRARY(x)
#else
// If the define is not set, we use static linking and symbols don't need to be imported or exported
#define JPH_EXPORT
#define JPH_IF_SHARED_LIBRARY(x)
#define JPH_IF_NOT_SHARED_LIBRARY(x) x
#endif
#ifndef JPH_EXPORT_GCC_BUG_WORKAROUND
#define JPH_EXPORT_GCC_BUG_WORKAROUND JPH_EXPORT
#endif
// Macro used by the RTTI macros to not export a function
#define JPH_NO_EXPORT
// Pragmas to store / restore the warning state and to disable individual warnings
#ifdef JPH_COMPILER_CLANG
#define JPH_PRAGMA(x) _Pragma(#x)
#define JPH_SUPPRESS_WARNING_PUSH JPH_PRAGMA(clang diagnostic push)
#define JPH_SUPPRESS_WARNING_POP JPH_PRAGMA(clang diagnostic pop)
#define JPH_CLANG_SUPPRESS_WARNING(w) JPH_PRAGMA(clang diagnostic ignored w)
#if __has_warning("-Wdeprecated-copy")
#define JPH_CLANG_SUPPRESS_DEPRECATED_COPY_WARNING JPH_CLANG_SUPPRESS_WARNING("-Wdeprecated-copy")
#endif
#if __has_warning("-Wdeprecated-copy-with-dtor")
#define JPH_CLANG_SUPPRESS_DEPRECATED_COPY_WITH_DTOR_WARNING JPH_CLANG_SUPPRESS_WARNING("-Wdeprecated-copy-with-dtor")
#endif
#if __has_warning("-Wunsafe-buffer-usage")
#define JPH_CLANG_SUPPRESS_UNSAFE_BUFFER_USAGE_WARNING JPH_CLANG_SUPPRESS_WARNING("-Wunsafe-buffer-usage")
#endif
#if __has_warning("-Wimplicit-int-float-conversion")
#define JPH_CLANG_SUPPRESS_IMPLICIT_INT_FLOAT_CONVERSION_WARNING JPH_CLANG_SUPPRESS_WARNING("-Wimplicit-int-float-conversion")
#endif
#if __has_warning("-Wunique-object-duplication")
#define JPH_CLANG_SUPPRESS_UNIQUE_OBJECT_DUPLICATION_WARNING JPH_CLANG_SUPPRESS_WARNING("-Wunique-object-duplication")
#endif
#if __has_warning("-Wnrvo")
#define JPH_CLANG_SUPPRESS_NRVO_WARNING JPH_CLANG_SUPPRESS_WARNING("-Wnrvo")
#endif
#else
#define JPH_CLANG_SUPPRESS_WARNING(w)
#endif
#ifndef JPH_CLANG_SUPPRESS_DEPRECATED_COPY_WARNING
#define JPH_CLANG_SUPPRESS_DEPRECATED_COPY_WARNING
#endif
#ifndef JPH_CLANG_SUPPRESS_DEPRECATED_COPY_WITH_DTOR_WARNING
#define JPH_CLANG_SUPPRESS_DEPRECATED_COPY_WITH_DTOR_WARNING
#endif
#ifndef JPH_CLANG_SUPPRESS_UNSAFE_BUFFER_USAGE_WARNING
#define JPH_CLANG_SUPPRESS_UNSAFE_BUFFER_USAGE_WARNING
#endif
#ifndef JPH_CLANG_SUPPRESS_IMPLICIT_INT_FLOAT_CONVERSION_WARNING
#define JPH_CLANG_SUPPRESS_IMPLICIT_INT_FLOAT_CONVERSION_WARNING
#endif
#ifndef JPH_CLANG_SUPPRESS_UNIQUE_OBJECT_DUPLICATION_WARNING
#define JPH_CLANG_SUPPRESS_UNIQUE_OBJECT_DUPLICATION_WARNING
#endif
#ifndef JPH_CLANG_SUPPRESS_NRVO_WARNING
#define JPH_CLANG_SUPPRESS_NRVO_WARNING
#endif
#ifdef JPH_COMPILER_GCC
#define JPH_PRAGMA(x) _Pragma(#x)
#define JPH_SUPPRESS_WARNING_PUSH JPH_PRAGMA(GCC diagnostic push)
#define JPH_SUPPRESS_WARNING_POP JPH_PRAGMA(GCC diagnostic pop)
#define JPH_GCC_SUPPRESS_WARNING(w) JPH_PRAGMA(GCC diagnostic ignored w)
#else
#define JPH_GCC_SUPPRESS_WARNING(w)
#endif
#ifdef JPH_COMPILER_MSVC
#define JPH_PRAGMA(x) __pragma(x)
#define JPH_SUPPRESS_WARNING_PUSH JPH_PRAGMA(warning (push))
#define JPH_SUPPRESS_WARNING_POP JPH_PRAGMA(warning (pop))
#define JPH_MSVC_SUPPRESS_WARNING(w) JPH_PRAGMA(warning (disable : w))
#if _MSC_VER >= 1920 && _MSC_VER < 1930
#define JPH_MSVC2019_SUPPRESS_WARNING(w) JPH_MSVC_SUPPRESS_WARNING(w)
#else
#define JPH_MSVC2019_SUPPRESS_WARNING(w)
#endif
#if _MSC_VER >= 1950
#define JPH_MSVC2026_PLUS_SUPPRESS_WARNING(w) JPH_MSVC_SUPPRESS_WARNING(w)
#else
#define JPH_MSVC2026_PLUS_SUPPRESS_WARNING(w)
#endif
#else
#define JPH_MSVC_SUPPRESS_WARNING(w)
#define JPH_MSVC2019_SUPPRESS_WARNING(w)
#define JPH_MSVC2026_PLUS_SUPPRESS_WARNING(w)
#endif
// Disable common warnings triggered by Jolt when compiling with -Wall
#define JPH_SUPPRESS_WARNINGS \
JPH_CLANG_SUPPRESS_WARNING("-Wc++98-compat") \
JPH_CLANG_SUPPRESS_WARNING("-Wc++98-compat-pedantic") \
JPH_CLANG_SUPPRESS_WARNING("-Wfloat-equal") \
JPH_CLANG_SUPPRESS_WARNING("-Wsign-conversion") \
JPH_CLANG_SUPPRESS_WARNING("-Wold-style-cast") \
JPH_CLANG_SUPPRESS_WARNING("-Wgnu-anonymous-struct") \
JPH_CLANG_SUPPRESS_WARNING("-Wnested-anon-types") \
JPH_CLANG_SUPPRESS_WARNING("-Wglobal-constructors") \
JPH_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors") \
JPH_CLANG_SUPPRESS_WARNING("-Wnonportable-system-include-path") \
JPH_CLANG_SUPPRESS_WARNING("-Wlanguage-extension-token") \
JPH_CLANG_SUPPRESS_WARNING("-Wunused-parameter") \
JPH_CLANG_SUPPRESS_WARNING("-Wformat-nonliteral") \
JPH_CLANG_SUPPRESS_WARNING("-Wcovered-switch-default") \
JPH_CLANG_SUPPRESS_WARNING("-Wcast-align") \
JPH_CLANG_SUPPRESS_WARNING("-Winvalid-offsetof") \
JPH_CLANG_SUPPRESS_WARNING("-Wgnu-zero-variadic-macro-arguments") \
JPH_CLANG_SUPPRESS_WARNING("-Wdocumentation-unknown-command") \
JPH_CLANG_SUPPRESS_WARNING("-Wctad-maybe-unsupported") \
JPH_CLANG_SUPPRESS_WARNING("-Wswitch-default") \
JPH_CLANG_SUPPRESS_DEPRECATED_COPY_WARNING \
JPH_CLANG_SUPPRESS_DEPRECATED_COPY_WITH_DTOR_WARNING \
JPH_CLANG_SUPPRESS_UNSAFE_BUFFER_USAGE_WARNING \
JPH_CLANG_SUPPRESS_IMPLICIT_INT_FLOAT_CONVERSION_WARNING \
JPH_CLANG_SUPPRESS_WARNING("-Wpadded") \
JPH_IF_NOT_SHARED_LIBRARY(JPH_CLANG_SUPPRESS_UNIQUE_OBJECT_DUPLICATION_WARNING) \
JPH_CLANG_SUPPRESS_NRVO_WARNING \
\
JPH_GCC_SUPPRESS_WARNING("-Wcomment") \
JPH_GCC_SUPPRESS_WARNING("-Winvalid-offsetof") \
JPH_GCC_SUPPRESS_WARNING("-Wclass-memaccess") \
JPH_GCC_SUPPRESS_WARNING("-Wpedantic") \
JPH_GCC_SUPPRESS_WARNING("-Wunused-parameter") \
JPH_GCC_SUPPRESS_WARNING("-Wmaybe-uninitialized") \
\
JPH_MSVC_SUPPRESS_WARNING(4619) /* #pragma warning: there is no warning number 'XXXX' */ \
JPH_MSVC_SUPPRESS_WARNING(4514) /* 'X' : unreferenced inline function has been removed */ \
JPH_MSVC_SUPPRESS_WARNING(4710) /* 'X' : function not inlined */ \
JPH_MSVC_SUPPRESS_WARNING(4711) /* function 'X' selected for automatic inline expansion */ \
JPH_MSVC_SUPPRESS_WARNING(4714) /* function 'X' marked as __forceinline not inlined */ \
JPH_MSVC_SUPPRESS_WARNING(4820) /* 'X': 'Y' bytes padding added after data member 'Z' */ \
JPH_MSVC_SUPPRESS_WARNING(4100) /* 'X' : unreferenced formal parameter */ \
JPH_MSVC_SUPPRESS_WARNING(4626) /* 'X' : assignment operator was implicitly defined as deleted because a base class assignment operator is inaccessible or deleted */ \
JPH_MSVC_SUPPRESS_WARNING(5027) /* 'X' : move assignment operator was implicitly defined as deleted because a base class move assignment operator is inaccessible or deleted */ \
JPH_MSVC_SUPPRESS_WARNING(4365) /* 'argument' : conversion from 'X' to 'Y', signed / unsigned mismatch */ \
JPH_MSVC_SUPPRESS_WARNING(4324) /* 'X' : structure was padded due to alignment specifier */ \
JPH_MSVC_SUPPRESS_WARNING(4625) /* 'X' : copy constructor was implicitly defined as deleted because a base class copy constructor is inaccessible or deleted */ \
JPH_MSVC_SUPPRESS_WARNING(5026) /* 'X': move constructor was implicitly defined as deleted because a base class move constructor is inaccessible or deleted */ \
JPH_MSVC_SUPPRESS_WARNING(4623) /* 'X' : default constructor was implicitly defined as deleted */ \
JPH_MSVC_SUPPRESS_WARNING(4201) /* nonstandard extension used: nameless struct/union */ \
JPH_MSVC_SUPPRESS_WARNING(4371) /* 'X': layout of class may have changed from a previous version of the compiler due to better packing of member 'Y' */ \
JPH_MSVC_SUPPRESS_WARNING(5045) /* Compiler will insert Spectre mitigation for memory load if /Qspectre switch specified */ \
JPH_MSVC_SUPPRESS_WARNING(4583) /* 'X': destructor is not implicitly called */ \
JPH_MSVC_SUPPRESS_WARNING(4582) /* 'X': constructor is not implicitly called */ \
JPH_MSVC_SUPPRESS_WARNING(5219) /* implicit conversion from 'X' to 'Y', possible loss of data */ \
JPH_MSVC_SUPPRESS_WARNING(4826) /* Conversion from 'X *' to 'JPH::uint64' is sign-extended. This may cause unexpected runtime behavior. (32-bit) */ \
JPH_MSVC_SUPPRESS_WARNING(5264) /* 'X': 'const' variable is not used */ \
JPH_MSVC_SUPPRESS_WARNING(4251) /* class 'X' needs to have DLL-interface to be used by clients of class 'Y' */ \
JPH_MSVC_SUPPRESS_WARNING(4738) /* storing 32-bit float result in memory, possible loss of performance */ \
JPH_MSVC2019_SUPPRESS_WARNING(5246) /* the initialization of a subobject should be wrapped in braces */ \
JPH_MSVC2026_PLUS_SUPPRESS_WARNING(5291) /* 'X': deriving from the base class 'Y' can cause potential runtime issues due to an ABI bug. Recommend adding a 4-byte data member to the base class for the padding at the end of it to work around this bug. */
// OS-specific includes
#if defined(JPH_PLATFORM_WINDOWS)
#define JPH_BREAKPOINT __debugbreak()
#elif defined(JPH_PLATFORM_BLUE)
// Configuration for a popular game console.
// This file is not distributed because it would violate an NDA.
// Creating one should only be a couple of minutes of work if you have the documentation for the platform
// (you only need to define JPH_BREAKPOINT, JPH_PLATFORM_BLUE_GET_TICKS, JPH_PLATFORM_BLUE_MUTEX*, JPH_PLATFORM_BLUE_RWLOCK*, JPH_PLATFORM_BLUE_SEMAPHORE* and include the right header).
#include <Jolt/Core/PlatformBlue.h>
#elif defined(JPH_PLATFORM_LINUX) || defined(JPH_PLATFORM_ANDROID) || defined(JPH_PLATFORM_MACOS) || defined(JPH_PLATFORM_IOS) || defined(JPH_PLATFORM_BSD)
#if defined(JPH_CPU_X86)
#define JPH_BREAKPOINT __asm volatile ("int $0x3")
#elif defined(JPH_CPU_ARM) || defined(JPH_CPU_RISCV) || defined(JPH_CPU_E2K) || defined(JPH_CPU_PPC) || defined(JPH_CPU_LOONGARCH)
#define JPH_BREAKPOINT __builtin_trap()
#else
#error Unknown CPU architecture
#endif
#elif defined(JPH_PLATFORM_WASM)
#define JPH_BREAKPOINT do { } while (false) // Not supported
#else
#error Unknown platform
#endif
// Begin the JPH namespace
#define JPH_NAMESPACE_BEGIN \
JPH_SUPPRESS_WARNING_PUSH \
JPH_SUPPRESS_WARNINGS \
namespace JPH {
// End the JPH namespace
#define JPH_NAMESPACE_END \
} \
JPH_SUPPRESS_WARNING_POP
// Suppress warnings generated by the standard template library
#define JPH_SUPPRESS_WARNINGS_STD_BEGIN \
JPH_SUPPRESS_WARNING_PUSH \
JPH_CLANG_SUPPRESS_WARNING("-Wpadded") \
JPH_MSVC_SUPPRESS_WARNING(4365) \
JPH_MSVC_SUPPRESS_WARNING(4619) \
JPH_MSVC_SUPPRESS_WARNING(4710) \
JPH_MSVC_SUPPRESS_WARNING(4711) \
JPH_MSVC_SUPPRESS_WARNING(4820) \
JPH_MSVC_SUPPRESS_WARNING(4514) \
JPH_MSVC_SUPPRESS_WARNING(5262) \
JPH_MSVC_SUPPRESS_WARNING(5264) \
JPH_MSVC_SUPPRESS_WARNING(4738) \
JPH_MSVC_SUPPRESS_WARNING(5045)
#define JPH_SUPPRESS_WARNINGS_STD_END \
JPH_SUPPRESS_WARNING_POP
// MSVC STL requires _HAS_EXCEPTIONS=0 if exceptions are turned off
#if defined(JPH_COMPILER_MSVC) && (!defined(__cpp_exceptions) || !__cpp_exceptions) && !defined(_HAS_EXCEPTIONS)
#define _HAS_EXCEPTIONS 0
#endif
// Standard C++ includes
JPH_SUPPRESS_WARNINGS_STD_BEGIN
#include <float.h>
#include <limits.h>
#include <string.h>
#include <new>
#include <utility>
#include <cmath>
#include <sstream>
#include <functional>
#include <algorithm>
#include <cstdint>
#include <type_traits>
#if defined(JPH_COMPILER_MSVC) || (defined(JPH_COMPILER_CLANG) && defined(_MSC_VER)) // MSVC or clang-cl
#include <malloc.h> // for alloca
#endif
#if defined(JPH_USE_SSE)
#include <immintrin.h>
#elif defined(JPH_USE_NEON)
#ifdef JPH_COMPILER_MSVC
#include <intrin.h>
#include <arm64_neon.h>
#else
#include <arm_neon.h>
#endif
#elif defined(JPH_USE_RVV)
#include <riscv_vector.h>
#endif
JPH_SUPPRESS_WARNINGS_STD_END
JPH_NAMESPACE_BEGIN
// Commonly used STL types
using std::min;
using std::max;
using std::abs;
using std::ceil;
using std::floor;
using std::trunc;
using std::round;
using std::fmod;
using std::string_view;
using std::function;
using std::numeric_limits;
using std::isfinite;
using std::isnan;
using std::ostream;
using std::istream;
// Standard types
using uint = unsigned int;
using uint8 = std::uint8_t;
using uint16 = std::uint16_t;
using uint32 = std::uint32_t;
using int32 = std::int32_t;
using uint64 = std::uint64_t;
using int64 = std::int64_t;
// Assert sizes of types
static_assert(sizeof(uint) >= 4, "Invalid size of uint");
static_assert(sizeof(uint8) == 1, "Invalid size of uint8");
static_assert(sizeof(uint16) == 2, "Invalid size of uint16");
static_assert(sizeof(uint32) == 4, "Invalid size of uint32");
static_assert(sizeof(uint64) == 8, "Invalid size of uint64");
// Determine if we want extra debugging code to be active
#if !defined(NDEBUG) && !defined(JPH_NO_DEBUG)
#define JPH_DEBUG
#endif
// Define inline macro
#if defined(JPH_NO_FORCE_INLINE)
#define JPH_INLINE inline
#elif defined(JPH_COMPILER_CLANG)
#define JPH_INLINE __inline__ __attribute__((always_inline))
#elif defined(JPH_COMPILER_GCC)
// On gcc 14 using always_inline in debug mode causes error: "inlining failed in call to 'always_inline' 'XXX': function not considered for inlining"
// See: https://github.com/jrouwe/JoltPhysics/issues/1096
#if __GNUC__ >= 14 && defined(JPH_DEBUG)
#define JPH_INLINE inline
#else
#define JPH_INLINE __inline__ __attribute__((always_inline))
#endif
#elif defined(JPH_COMPILER_MSVC)
#define JPH_INLINE __forceinline
#else
#error Undefined
#endif
// Default memory allocation alignment.
// This define can be overridden in case the user provides an Allocate function that has a different alignment than the platform default.
#ifndef JPH_DEFAULT_ALLOCATE_ALIGNMENT
#define JPH_DEFAULT_ALLOCATE_ALIGNMENT __STDCPP_DEFAULT_NEW_ALIGNMENT__
#endif
// Cache line size (used for aligning to cache line)
#ifndef JPH_CACHE_LINE_SIZE
#define JPH_CACHE_LINE_SIZE 64
#endif
// Define macro to get current function name
#if defined(JPH_COMPILER_CLANG) || defined(JPH_COMPILER_GCC)
#define JPH_FUNCTION_NAME __PRETTY_FUNCTION__
#elif defined(JPH_COMPILER_MSVC)
#define JPH_FUNCTION_NAME __FUNCTION__
#else
#error Undefined
#endif
// Stack allocation
#define JPH_STACK_ALLOC(n) alloca(n)
// Shorthand for #ifdef JPH_DEBUG / #endif
#ifdef JPH_DEBUG
#define JPH_IF_DEBUG(...) __VA_ARGS__
#define JPH_IF_NOT_DEBUG(...)
#else
#define JPH_IF_DEBUG(...)
#define JPH_IF_NOT_DEBUG(...) __VA_ARGS__
#endif
// Shorthand for #ifdef JPH_FLOATING_POINT_EXCEPTIONS_ENABLED / #endif
#ifdef JPH_FLOATING_POINT_EXCEPTIONS_ENABLED
#define JPH_IF_FLOATING_POINT_EXCEPTIONS_ENABLED(...) __VA_ARGS__
#else
#define JPH_IF_FLOATING_POINT_EXCEPTIONS_ENABLED(...)
#endif
// Helper macros to detect if we're running in single or double precision mode
#ifdef JPH_DOUBLE_PRECISION
#define JPH_IF_SINGLE_PRECISION(...)
#define JPH_IF_SINGLE_PRECISION_ELSE(s, d) d
#define JPH_IF_DOUBLE_PRECISION(...) __VA_ARGS__
#else
#define JPH_IF_SINGLE_PRECISION(...) __VA_ARGS__
#define JPH_IF_SINGLE_PRECISION_ELSE(s, d) s
#define JPH_IF_DOUBLE_PRECISION(...)
#endif
// Helper macro to detect if the debug renderer is active
#ifdef JPH_DEBUG_RENDERER
#define JPH_IF_DEBUG_RENDERER(...) __VA_ARGS__
#define JPH_IF_NOT_DEBUG_RENDERER(...)
#else
#define JPH_IF_DEBUG_RENDERER(...)
#define JPH_IF_NOT_DEBUG_RENDERER(...) __VA_ARGS__
#endif
// Macro to indicate that a parameter / variable is unused
#define JPH_UNUSED(x) (void)x
// Macro to enable floating point precise mode
#if defined(JPH_COMPILER_CLANG)
#define JPH_PRECISE_MATH_ON \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Wignored-pragmas\"") \
_Pragma("float_control(precise, on, push)") \
_Pragma("clang diagnostic pop")
#define JPH_PRECISE_MATH_OFF \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Wignored-pragmas\"") \
_Pragma("float_control(pop)") \
_Pragma("clang diagnostic pop")
#elif defined(JPH_COMPILER_MSVC)
#define JPH_PRECISE_MATH_ON \
__pragma(float_control(precise, on, push))
#define JPH_PRECISE_MATH_OFF \
__pragma(float_control(pop))
#else
#define JPH_PRECISE_MATH_ON
#define JPH_PRECISE_MATH_OFF
#endif
// Check if Thread Sanitizer is enabled
#ifdef __has_feature
#if __has_feature(thread_sanitizer)
#define JPH_TSAN_ENABLED
#endif
#else
#ifdef __SANITIZE_THREAD__
#define JPH_TSAN_ENABLED
#endif
#endif
// Attribute to disable Thread Sanitizer for a particular function
#ifdef JPH_TSAN_ENABLED
#define JPH_TSAN_NO_SANITIZE __attribute__((no_sanitize("thread")))
#else
#define JPH_TSAN_NO_SANITIZE
#endif
// Check if Address Sanitizer is enabled
#ifdef __has_feature
#if __has_feature(address_sanitizer)
#define JPH_ASAN_ENABLED
#endif
#else
#ifdef __SANITIZE_ADDRESS__
#define JPH_ASAN_ENABLED
#endif
#endif
// DirectX 12 is only supported on Windows
#if defined(JPH_USE_DX12) && !defined(JPH_PLATFORM_WINDOWS)
#undef JPH_USE_DX12
#endif // JPH_PLATFORM_WINDOWS
// Metal is only supported on Apple platforms
#if defined(JPH_USE_MTL) && !defined(JPH_PLATFORM_MACOS) && !defined(JPH_PLATFORM_IOS)
#undef JPH_USE_MTL
#endif // !JPH_PLATFORM_MACOS && !JPH_PLATFORM_IOS
JPH_NAMESPACE_END

View file

@ -0,0 +1,143 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/Core/NonCopyable.h>
JPH_NAMESPACE_BEGIN
#if defined(JPH_CPU_WASM)
// Not supported
#elif defined(JPH_USE_SSE)
/// Helper class that needs to be put on the stack to update the state of the floating point control word.
/// This state is kept per thread.
template <uint Value, uint Mask>
class FPControlWord : public NonCopyable
{
public:
FPControlWord()
{
mPrevState = _mm_getcsr();
_mm_setcsr((mPrevState & ~Mask) | Value);
}
~FPControlWord()
{
_mm_setcsr((_mm_getcsr() & ~Mask) | (mPrevState & Mask));
}
private:
uint mPrevState;
};
#elif defined(JPH_CPU_ARM) && defined(JPH_COMPILER_MSVC)
/// Helper class that needs to be put on the stack to update the state of the floating point control word.
/// This state is kept per thread.
template <unsigned int Value, unsigned int Mask>
class FPControlWord : public NonCopyable
{
public:
FPControlWord()
{
// Read state before change
_controlfp_s(&mPrevState, 0, 0);
// Update the state
unsigned int dummy;
_controlfp_s(&dummy, Value, Mask);
}
~FPControlWord()
{
// Restore state
unsigned int dummy;
_controlfp_s(&dummy, mPrevState, Mask);
}
private:
unsigned int mPrevState;
};
#elif defined(JPH_CPU_ARM) && defined(JPH_USE_NEON)
/// Helper class that needs to be put on the stack to update the state of the floating point control word.
/// This state is kept per thread.
template <uint64 Value, uint64 Mask>
class FPControlWord : public NonCopyable
{
public:
FPControlWord()
{
uint64 val;
asm volatile("mrs %0, fpcr" : "=r" (val));
mPrevState = val;
val &= ~Mask;
val |= Value;
asm volatile("msr fpcr, %0" : /* no output */ : "r" (val));
}
~FPControlWord()
{
uint64 val;
asm volatile("mrs %0, fpcr" : "=r" (val));
val &= ~Mask;
val |= mPrevState & Mask;
asm volatile("msr fpcr, %0" : /* no output */ : "r" (val));
}
private:
uint64 mPrevState;
};
#elif defined(JPH_CPU_ARM)
/// Helper class that needs to be put on the stack to update the state of the floating point control word.
/// This state is kept per thread.
template <uint32 Value, uint32 Mask>
class FPControlWord : public NonCopyable
{
public:
FPControlWord()
{
uint32 val;
asm volatile("vmrs %0, fpscr" : "=r" (val));
mPrevState = val;
val &= ~Mask;
val |= Value;
asm volatile("vmsr fpscr, %0" : /* no output */ : "r" (val));
}
~FPControlWord()
{
uint32 val;
asm volatile("vmrs %0, fpscr" : "=r" (val));
val &= ~Mask;
val |= mPrevState & Mask;
asm volatile("vmsr fpscr, %0" : /* no output */ : "r" (val));
}
private:
uint32 mPrevState;
};
#elif defined(JPH_CPU_RISCV)
// RISC-V only implements manually checking if exceptions occurred by reading the fcsr register. It doesn't generate exceptions.
#elif defined(JPH_CPU_PPC) || defined(JPH_CPU_LOONGARCH)
// Not implemented right now
#else
#error Unsupported CPU architecture
#endif
JPH_NAMESPACE_END

View file

@ -0,0 +1,96 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/Core/FPControlWord.h>
JPH_NAMESPACE_BEGIN
#ifdef JPH_FLOATING_POINT_EXCEPTIONS_ENABLED
#if defined(JPH_CPU_WASM)
// Not supported
class FPExceptionsEnable { };
class FPExceptionDisableInvalid { };
class FPExceptionDisableDivByZero { };
class FPExceptionDisableOverflow { };
#elif defined(JPH_USE_SSE)
/// Enable floating point divide by zero exception, overflow exceptions and exceptions on invalid numbers
class FPExceptionsEnable : public FPControlWord<0, _MM_MASK_DIV_ZERO | _MM_MASK_INVALID | _MM_MASK_OVERFLOW> { };
/// Disable invalid floating point value exceptions
class FPExceptionDisableInvalid : public FPControlWord<_MM_MASK_INVALID, _MM_MASK_INVALID> { };
/// Disable division by zero floating point exceptions
class FPExceptionDisableDivByZero : public FPControlWord<_MM_MASK_DIV_ZERO, _MM_MASK_DIV_ZERO> { };
/// Disable floating point overflow exceptions
class FPExceptionDisableOverflow : public FPControlWord<_MM_MASK_OVERFLOW, _MM_MASK_OVERFLOW> { };
#elif defined(JPH_CPU_ARM) && defined(JPH_COMPILER_MSVC)
/// Enable floating point divide by zero exception, overflow exceptions and exceptions on invalid numbers
class FPExceptionsEnable : public FPControlWord<0, _EM_INVALID | _EM_ZERODIVIDE | _EM_OVERFLOW> { };
/// Disable invalid floating point value exceptions
class FPExceptionDisableInvalid : public FPControlWord<_EM_INVALID, _EM_INVALID> { };
/// Disable division by zero floating point exceptions
class FPExceptionDisableDivByZero : public FPControlWord<_EM_ZERODIVIDE, _EM_ZERODIVIDE> { };
/// Disable floating point overflow exceptions
class FPExceptionDisableOverflow : public FPControlWord<_EM_OVERFLOW, _EM_OVERFLOW> { };
#elif defined(JPH_CPU_ARM)
/// Invalid operation exception bit
static constexpr uint64 FP_IOE = 1 << 8;
/// Enable divide by zero exception bit
static constexpr uint64 FP_DZE = 1 << 9;
/// Enable floating point overflow bit
static constexpr uint64 FP_OFE = 1 << 10;
/// Enable floating point divide by zero exception, overflow exceptions and exceptions on invalid numbers
class FPExceptionsEnable : public FPControlWord<FP_IOE | FP_DZE | FP_OFE, FP_IOE | FP_DZE | FP_OFE> { };
/// Disable invalid floating point value exceptions
class FPExceptionDisableInvalid : public FPControlWord<0, FP_IOE> { };
/// Disable division by zero floating point exceptions
class FPExceptionDisableDivByZero : public FPControlWord<0, FP_DZE> { };
/// Disable floating point overflow exceptions
class FPExceptionDisableOverflow : public FPControlWord<0, FP_OFE> { };
#elif defined(JPH_CPU_RISCV)
#error "RISC-V only implements manually checking if exceptions occurred by reading the fcsr register. It doesn't generate exceptions. JPH_FLOATING_POINT_EXCEPTIONS_ENABLED must be disabled."
#elif defined(JPH_CPU_PPC)
#error PowerPC floating point exception handling to be implemented. JPH_FLOATING_POINT_EXCEPTIONS_ENABLED must be disabled.
#else
#error Unsupported CPU architecture
#endif
#else
/// Dummy implementations
class FPExceptionsEnable { };
class FPExceptionDisableInvalid { };
class FPExceptionDisableDivByZero { };
class FPExceptionDisableOverflow { };
#endif
JPH_NAMESPACE_END

View file

@ -0,0 +1,43 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/Core/FPControlWord.h>
JPH_NAMESPACE_BEGIN
#if defined(JPH_CPU_WASM) || defined(JPH_CPU_RISCV) || defined(JPH_CPU_PPC) || defined(JPH_CPU_LOONGARCH)
// Not supported
class FPFlushDenormals { };
#elif defined(JPH_USE_SSE)
/// Helper class that needs to be put on the stack to enable flushing denormals to zero
/// This can make floating point operations much faster when working with very small numbers
class FPFlushDenormals : public FPControlWord<_MM_FLUSH_ZERO_ON, _MM_FLUSH_ZERO_MASK> { };
#elif defined(JPH_CPU_ARM) && defined(JPH_COMPILER_MSVC)
/// Helper class that needs to be put on the stack to enable flushing denormals to zero
/// This can make floating point operations much faster when working with very small numbers
class FPFlushDenormals : public FPControlWord<_DN_FLUSH, _MCW_DN> { };
#elif defined(JPH_CPU_ARM)
/// Flush denormals to zero bit
static constexpr uint64 FP_FZ = 1 << 24;
/// Helper class that needs to be put on the stack to enable flushing denormals to zero
/// This can make floating point operations much faster when working with very small numbers
class FPFlushDenormals : public FPControlWord<FP_FZ, FP_FZ> { };
#else
#error Unsupported CPU architecture
#endif
JPH_NAMESPACE_END

View file

@ -0,0 +1,92 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <Jolt/Jolt.h>
#include <Jolt/Core/Factory.h>
JPH_NAMESPACE_BEGIN
Factory *Factory::sInstance = nullptr;
void *Factory::CreateObject(const char *inName)
{
const RTTI *ci = Find(inName);
return ci != nullptr? ci->CreateObject() : nullptr;
}
const RTTI *Factory::Find(const char *inName)
{
ClassNameMap::iterator c = mClassNameMap.find(inName);
return c != mClassNameMap.end()? c->second : nullptr;
}
const RTTI *Factory::Find(uint32 inHash)
{
ClassHashMap::iterator c = mClassHashMap.find(inHash);
return c != mClassHashMap.end()? c->second : nullptr;
}
bool Factory::Register(const RTTI *inRTTI)
{
// Check if we already know the type
if (Find(inRTTI->GetName()) != nullptr)
return true;
// Insert this class by name
mClassNameMap.try_emplace(inRTTI->GetName(), inRTTI);
// Insert this class by hash
if (!mClassHashMap.try_emplace(inRTTI->GetHash(), inRTTI).second)
{
JPH_ASSERT(false, "Hash collision registering type!");
return false;
}
// Register base classes
for (int i = 0; i < inRTTI->GetBaseClassCount(); ++i)
if (!Register(inRTTI->GetBaseClass(i)))
return false;
#ifdef JPH_OBJECT_STREAM
// Register attribute classes
for (int i = 0; i < inRTTI->GetAttributeCount(); ++i)
{
const RTTI *rtti = inRTTI->GetAttribute(i).GetMemberPrimitiveType();
if (rtti != nullptr && !Register(rtti))
return false;
}
#endif // JPH_OBJECT_STREAM
return true;
}
bool Factory::Register(const RTTI **inRTTIs, uint inNumber)
{
mClassHashMap.reserve(mClassHashMap.size() + inNumber);
mClassNameMap.reserve(mClassNameMap.size() + inNumber);
for (const RTTI **rtti = inRTTIs; rtti < inRTTIs + inNumber; ++rtti)
if (!Register(*rtti))
return false;
return true;
}
void Factory::Clear()
{
mClassNameMap.clear();
mClassHashMap.clear();
}
Array<const RTTI *> Factory::GetAllClasses() const
{
Array<const RTTI *> all_classes;
all_classes.reserve(mClassNameMap.size());
for (const ClassNameMap::value_type &c : mClassNameMap)
all_classes.push_back(c.second);
return all_classes;
}
JPH_NAMESPACE_END

View file

@ -0,0 +1,54 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/Core/RTTI.h>
#include <Jolt/Core/UnorderedMap.h>
JPH_NAMESPACE_BEGIN
/// This class is responsible for creating instances of classes based on their name or hash and is mainly used for deserialization of saved data.
class JPH_EXPORT Factory
{
public:
JPH_OVERRIDE_NEW_DELETE
/// Create an object
void * CreateObject(const char *inName);
/// Find type info for a specific class by name
const RTTI * Find(const char *inName);
/// Find type info for a specific class by hash
const RTTI * Find(uint32 inHash);
/// Register an object with the factory. Returns false on failure.
bool Register(const RTTI *inRTTI);
/// Register a list of objects with the factory. Returns false on failure.
bool Register(const RTTI **inRTTIs, uint inNumber);
/// Unregisters all types
void Clear();
/// Get all registered classes
Array<const RTTI *> GetAllClasses() const;
/// Singleton factory instance
static Factory * sInstance;
private:
using ClassNameMap = UnorderedMap<string_view, const RTTI *>;
using ClassHashMap = UnorderedMap<uint32, const RTTI *>;
/// Map of class names to type info
ClassNameMap mClassNameMap;
// Map of class hash to type info
ClassHashMap mClassHashMap;
};
JPH_NAMESPACE_END

View file

@ -0,0 +1,122 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/Core/NonCopyable.h>
#include <Jolt/Core/Mutex.h>
#include <Jolt/Core/Atomics.h>
JPH_NAMESPACE_BEGIN
/// Class that allows lock free creation / destruction of objects (unless a new page of objects needs to be allocated)
/// It contains a fixed pool of objects and also allows batching up a lot of objects to be destroyed
/// and doing the actual free in a single atomic operation
template <typename Object>
class FixedSizeFreeList : public NonCopyable
{
private:
/// Storage type for an Object
struct ObjectStorage
{
/// The object we're storing
Object mObject;
/// When the object is freed (or in the process of being freed as a batch) this will contain the next free object
/// When an object is in use it will contain the object's index in the free list
atomic<uint32> mNextFreeObject;
};
static_assert(alignof(ObjectStorage) == alignof(Object), "Object not properly aligned");
/// Access the object storage given the object index
const ObjectStorage & GetStorage(uint32 inObjectIndex) const { return mPages[inObjectIndex >> mPageShift][inObjectIndex & mObjectMask]; }
ObjectStorage & GetStorage(uint32 inObjectIndex) { return mPages[inObjectIndex >> mPageShift][inObjectIndex & mObjectMask]; }
/// Size (in objects) of a single page
uint32 mPageSize;
/// Number of bits to shift an object index to the right to get the page number
uint32 mPageShift;
/// Mask to and an object index with to get the page number
uint32 mObjectMask;
/// Total number of pages that are usable
uint32 mNumPages;
/// Total number of objects that have been allocated
uint32 mNumObjectsAllocated;
/// Array of pages of objects
ObjectStorage ** mPages = nullptr;
/// Mutex that is used to allocate a new page if the storage runs out
/// This variable is aligned to the cache line to prevent false sharing with
/// the constants used to index into the list via `Get()`.
alignas(JPH_CACHE_LINE_SIZE) Mutex mPageMutex;
/// Number of objects that we currently have in the free list / new pages
#ifdef JPH_ENABLE_ASSERTS
atomic<uint32> mNumFreeObjects;
#endif // JPH_ENABLE_ASSERTS
/// Simple counter that makes the first free object pointer update with every CAS so that we don't suffer from the ABA problem
atomic<uint32> mAllocationTag;
/// Index of first free object, the first 32 bits of an object are used to point to the next free object
atomic<uint64> mFirstFreeObjectAndTag;
/// The first free object to use when the free list is empty (may need to allocate a new page)
atomic<uint32> mFirstFreeObjectInNewPage;
public:
/// Invalid index
static const uint32 cInvalidObjectIndex = 0xffffffff;
/// Size of an object + bookkeeping for the freelist
static const int ObjectStorageSize = sizeof(ObjectStorage);
/// Destructor
inline ~FixedSizeFreeList();
/// Initialize the free list, up to inMaxObjects can be allocated
inline void Init(uint inMaxObjects, uint inPageSize);
/// Lockless construct a new object, inParameters are passed on to the constructor
template <typename... Parameters>
inline uint32 ConstructObject(Parameters &&... inParameters);
/// Lockless destruct an object and return it to the free pool
inline void DestructObject(uint32 inObjectIndex);
/// Lockless destruct an object and return it to the free pool
inline void DestructObject(Object *inObject);
/// A batch of objects that can be destructed
struct Batch
{
uint32 mFirstObjectIndex = cInvalidObjectIndex;
uint32 mLastObjectIndex = cInvalidObjectIndex;
uint32 mNumObjects = 0;
};
/// Add a object to an existing batch to be destructed.
/// Adding objects to a batch does not destroy or modify the objects, this will merely link them
/// so that the entire batch can be returned to the free list in a single atomic operation
inline void AddObjectToBatch(Batch &ioBatch, uint32 inObjectIndex);
/// Lockless destruct batch of objects
inline void DestructObjectBatch(Batch &ioBatch);
/// Access an object by index.
inline Object & Get(uint32 inObjectIndex) { return GetStorage(inObjectIndex).mObject; }
/// Access an object by index.
inline const Object & Get(uint32 inObjectIndex) const { return GetStorage(inObjectIndex).mObject; }
};
JPH_NAMESPACE_END
#include "FixedSizeFreeList.inl"

View file

@ -0,0 +1,215 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
JPH_NAMESPACE_BEGIN
template <typename Object>
FixedSizeFreeList<Object>::~FixedSizeFreeList()
{
// Check if we got our Init call
if (mPages != nullptr)
{
// Ensure everything is freed before the freelist is destructed
JPH_ASSERT(mNumFreeObjects.load(memory_order_relaxed) == mNumPages * mPageSize);
// Free memory for pages
uint32 num_pages = mNumObjectsAllocated / mPageSize;
for (uint32 page = 0; page < num_pages; ++page)
AlignedFree(mPages[page]);
Free(mPages);
}
}
template <typename Object>
void FixedSizeFreeList<Object>::Init(uint inMaxObjects, uint inPageSize)
{
// Check sanity
JPH_ASSERT(inPageSize > 0 && IsPowerOf2(inPageSize));
JPH_ASSERT(mPages == nullptr);
// Store configuration parameters
mNumPages = (inMaxObjects + inPageSize - 1) / inPageSize;
mPageSize = inPageSize;
mPageShift = CountTrailingZeros(inPageSize);
mObjectMask = inPageSize - 1;
JPH_IF_ENABLE_ASSERTS(mNumFreeObjects = mNumPages * inPageSize;)
// Allocate page table
mPages = reinterpret_cast<ObjectStorage **>(Allocate(mNumPages * sizeof(ObjectStorage *)));
// We didn't yet use any objects of any page
mNumObjectsAllocated = 0;
mFirstFreeObjectInNewPage = 0;
// Start with 1 as the first tag
mAllocationTag = 1;
// Set first free object (with tag 0)
mFirstFreeObjectAndTag = cInvalidObjectIndex;
}
template <typename Object>
template <typename... Parameters>
uint32 FixedSizeFreeList<Object>::ConstructObject(Parameters &&... inParameters)
{
for (;;)
{
// Get first object from the linked list
uint64 first_free_object_and_tag = mFirstFreeObjectAndTag.load(memory_order_acquire);
uint32 first_free = uint32(first_free_object_and_tag);
if (first_free == cInvalidObjectIndex)
{
// The free list is empty, we take an object from the page that has never been used before
first_free = mFirstFreeObjectInNewPage.fetch_add(1, memory_order_relaxed);
if (first_free >= mNumObjectsAllocated)
{
// Allocate new page
lock_guard lock(mPageMutex);
while (first_free >= mNumObjectsAllocated)
{
uint32 next_page = mNumObjectsAllocated / mPageSize;
if (next_page == mNumPages)
return cInvalidObjectIndex; // Out of space!
mPages[next_page] = reinterpret_cast<ObjectStorage *>(AlignedAllocate(mPageSize * sizeof(ObjectStorage), max<size_t>(alignof(ObjectStorage), JPH_CACHE_LINE_SIZE)));
mNumObjectsAllocated += mPageSize;
}
}
// Allocation successful
JPH_IF_ENABLE_ASSERTS(mNumFreeObjects.fetch_sub(1, memory_order_relaxed);)
ObjectStorage &storage = GetStorage(first_free);
new (&storage.mObject) Object(std::forward<Parameters>(inParameters)...);
storage.mNextFreeObject.store(first_free, memory_order_release);
return first_free;
}
else
{
// Load next pointer
uint32 new_first_free = GetStorage(first_free).mNextFreeObject.load(memory_order_acquire);
// Construct a new first free object tag
uint64 new_first_free_object_and_tag = uint64(new_first_free) + (uint64(mAllocationTag.fetch_add(1, memory_order_relaxed)) << 32);
// Compare and swap
if (mFirstFreeObjectAndTag.compare_exchange_weak(first_free_object_and_tag, new_first_free_object_and_tag, memory_order_release))
{
// Allocation successful
JPH_IF_ENABLE_ASSERTS(mNumFreeObjects.fetch_sub(1, memory_order_relaxed);)
ObjectStorage &storage = GetStorage(first_free);
new (&storage.mObject) Object(std::forward<Parameters>(inParameters)...);
storage.mNextFreeObject.store(first_free, memory_order_release);
return first_free;
}
}
}
}
template <typename Object>
void FixedSizeFreeList<Object>::AddObjectToBatch(Batch &ioBatch, uint32 inObjectIndex)
{
JPH_ASSERT(ioBatch.mNumObjects != uint32(-1), "Trying to reuse a batch that has already been freed");
// Reset next index
atomic<uint32> &next_free_object = GetStorage(inObjectIndex).mNextFreeObject;
JPH_ASSERT(next_free_object.load(memory_order_relaxed) == inObjectIndex, "Trying to add a object to the batch that is already in a free list");
next_free_object.store(cInvalidObjectIndex, memory_order_release);
// Link object in batch to free
if (ioBatch.mFirstObjectIndex == cInvalidObjectIndex)
ioBatch.mFirstObjectIndex = inObjectIndex;
else
GetStorage(ioBatch.mLastObjectIndex).mNextFreeObject.store(inObjectIndex, memory_order_release);
ioBatch.mLastObjectIndex = inObjectIndex;
ioBatch.mNumObjects++;
}
template <typename Object>
void FixedSizeFreeList<Object>::DestructObjectBatch(Batch &ioBatch)
{
if (ioBatch.mFirstObjectIndex != cInvalidObjectIndex)
{
// Call destructors
if constexpr (!std::is_trivially_destructible<Object>())
{
uint32 object_idx = ioBatch.mFirstObjectIndex;
do
{
ObjectStorage &storage = GetStorage(object_idx);
storage.mObject.~Object();
object_idx = storage.mNextFreeObject.load(memory_order_relaxed);
}
while (object_idx != cInvalidObjectIndex);
}
// Add to objects free list
ObjectStorage &storage = GetStorage(ioBatch.mLastObjectIndex);
for (;;)
{
// Get first object from the list
uint64 first_free_object_and_tag = mFirstFreeObjectAndTag.load(memory_order_acquire);
uint32 first_free = uint32(first_free_object_and_tag);
// Make it the next pointer of the last object in the batch that is to be freed
storage.mNextFreeObject.store(first_free, memory_order_release);
// Construct a new first free object tag
uint64 new_first_free_object_and_tag = uint64(ioBatch.mFirstObjectIndex) + (uint64(mAllocationTag.fetch_add(1, memory_order_relaxed)) << 32);
// Compare and swap
if (mFirstFreeObjectAndTag.compare_exchange_weak(first_free_object_and_tag, new_first_free_object_and_tag, memory_order_release))
{
// Free successful
JPH_IF_ENABLE_ASSERTS(mNumFreeObjects.fetch_add(ioBatch.mNumObjects, memory_order_relaxed);)
// Mark the batch as freed
#ifdef JPH_ENABLE_ASSERTS
ioBatch.mNumObjects = uint32(-1);
#endif
return;
}
}
}
}
template <typename Object>
void FixedSizeFreeList<Object>::DestructObject(uint32 inObjectIndex)
{
JPH_ASSERT(inObjectIndex != cInvalidObjectIndex);
// Call destructor
ObjectStorage &storage = GetStorage(inObjectIndex);
storage.mObject.~Object();
// Add to object free list
for (;;)
{
// Get first object from the list
uint64 first_free_object_and_tag = mFirstFreeObjectAndTag.load(memory_order_acquire);
uint32 first_free = uint32(first_free_object_and_tag);
// Make it the next pointer of the last object in the batch that is to be freed
storage.mNextFreeObject.store(first_free, memory_order_release);
// Construct a new first free object tag
uint64 new_first_free_object_and_tag = uint64(inObjectIndex) + (uint64(mAllocationTag.fetch_add(1, memory_order_relaxed)) << 32);
// Compare and swap
if (mFirstFreeObjectAndTag.compare_exchange_weak(first_free_object_and_tag, new_first_free_object_and_tag, memory_order_release))
{
// Free successful
JPH_IF_ENABLE_ASSERTS(mNumFreeObjects.fetch_add(1, memory_order_relaxed);)
return;
}
}
}
template<typename Object>
inline void FixedSizeFreeList<Object>::DestructObject(Object *inObject)
{
uint32 index = reinterpret_cast<ObjectStorage *>(inObject)->mNextFreeObject.load(memory_order_relaxed);
JPH_ASSERT(index < mNumObjectsAllocated);
DestructObject(index);
}
JPH_NAMESPACE_END

View file

@ -0,0 +1,234 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
JPH_NAMESPACE_BEGIN
/// Implements the FNV-1a hash algorithm
/// @see https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
/// @param inData Data block of bytes
/// @param inSize Number of bytes
/// @param inSeed Seed of the hash (can be used to pass in the hash of a previous operation, otherwise leave default)
/// @return Hash
inline uint64 HashBytes(const void *inData, uint inSize, uint64 inSeed = 0xcbf29ce484222325UL)
{
uint64 hash = inSeed;
for (const uint8 *data = reinterpret_cast<const uint8 *>(inData); data < reinterpret_cast<const uint8 *>(inData) + inSize; ++data)
{
hash ^= uint64(*data);
hash *= 0x100000001b3UL;
}
return hash;
}
/// Calculate the FNV-1a hash of inString.
/// @see https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
constexpr uint64 HashString(const char *inString, uint64 inSeed = 0xcbf29ce484222325UL)
{
uint64 hash = inSeed;
for (const char *c = inString; *c != 0; ++c)
{
hash ^= uint64(*c);
hash *= 0x100000001b3UL;
}
return hash;
}
/// A 64 bit hash function by Thomas Wang, Jan 1997
/// See: http://web.archive.org/web/20071223173210/http://www.concentric.net/~Ttwang/tech/inthash.htm
/// @param inValue Value to hash
/// @return Hash
inline uint64 Hash64(uint64 inValue)
{
uint64 hash = inValue;
hash = (~hash) + (hash << 21); // hash = (hash << 21) - hash - 1;
hash = hash ^ (hash >> 24);
hash = (hash + (hash << 3)) + (hash << 8); // hash * 265
hash = hash ^ (hash >> 14);
hash = (hash + (hash << 2)) + (hash << 4); // hash * 21
hash = hash ^ (hash >> 28);
hash = hash + (hash << 31);
return hash;
}
/// Fallback hash function that calls T::GetHash()
template <class T>
struct Hash
{
uint64 operator () (const T &inValue) const
{
return inValue.GetHash();
}
};
/// A hash function for floats
template <>
struct Hash<float>
{
uint64 operator () (float inValue) const
{
float value = inValue == 0.0f? 0.0f : inValue; // Convert -0.0f to 0.0f
return HashBytes(&value, sizeof(value));
}
};
/// A hash function for doubles
template <>
struct Hash<double>
{
uint64 operator () (double inValue) const
{
double value = inValue == 0.0? 0.0 : inValue; // Convert -0.0 to 0.0
return HashBytes(&value, sizeof(value));
}
};
/// A hash function for character pointers
template <>
struct Hash<const char *>
{
uint64 operator () (const char *inValue) const
{
return HashString(inValue);
}
};
/// A hash function for std::string_view
template <>
struct Hash<std::string_view>
{
uint64 operator () (const std::string_view &inValue) const
{
return HashBytes(inValue.data(), uint(inValue.size()));
}
};
/// A hash function for String
template <>
struct Hash<String>
{
uint64 operator () (const String &inValue) const
{
return HashBytes(inValue.data(), uint(inValue.size()));
}
};
/// A fallback function for generic pointers
template <class T>
struct Hash<T *>
{
uint64 operator () (T *inValue) const
{
return HashBytes(&inValue, sizeof(inValue));
}
};
/// Helper macro to define a hash function for trivial types
#define JPH_DEFINE_TRIVIAL_HASH(type) \
template <> \
struct Hash<type> \
{ \
uint64 operator () (const type &inValue) const \
{ \
return HashBytes(&inValue, sizeof(inValue)); \
} \
};
/// Commonly used types
JPH_DEFINE_TRIVIAL_HASH(char)
JPH_DEFINE_TRIVIAL_HASH(int)
JPH_DEFINE_TRIVIAL_HASH(uint32)
JPH_DEFINE_TRIVIAL_HASH(uint64)
/// Helper function that hashes a single value into ioSeed
/// Based on https://github.com/jonmaiga/mx3 by Jon Maiga
template <typename T>
inline void HashCombine(uint64 &ioSeed, const T &inValue)
{
constexpr uint64 c = 0xbea225f9eb34556dUL;
uint64 h = ioSeed;
uint64 x = Hash<T> { } (inValue);
// See: https://github.com/jonmaiga/mx3/blob/master/mx3.h
// mix_stream(h, x)
x *= c;
x ^= x >> 39;
h += x * c;
h *= c;
// mix(h)
h ^= h >> 32;
h *= c;
h ^= h >> 29;
h *= c;
h ^= h >> 32;
h *= c;
h ^= h >> 29;
ioSeed = h;
}
/// Hash combiner to use a custom struct in an unordered map or set
///
/// Usage:
///
/// struct SomeHashKey
/// {
/// std::string key1;
/// std::string key2;
/// bool key3;
/// };
///
/// JPH_MAKE_HASHABLE(SomeHashKey, t.key1, t.key2, t.key3)
template <typename FirstValue, typename... Values>
inline uint64 HashCombineArgs(const FirstValue &inFirstValue, Values... inValues)
{
// Prime the seed by hashing the first value
uint64 seed = Hash<FirstValue> { } (inFirstValue);
// Hash all remaining values together using a fold expression
(HashCombine(seed, inValues), ...);
return seed;
}
#define JPH_MAKE_HASH_STRUCT(type, name, ...) \
struct [[nodiscard]] name \
{ \
::JPH::uint64 operator()(const type &t) const \
{ \
return ::JPH::HashCombineArgs(__VA_ARGS__); \
} \
};
#define JPH_MAKE_STD_HASH(type) \
JPH_SUPPRESS_WARNING_PUSH \
JPH_SUPPRESS_WARNINGS \
namespace std \
{ \
template<> \
struct [[nodiscard]] hash<type> \
{ \
size_t operator()(const type &t) const \
{ \
return size_t(::JPH::Hash<type>{ }(t)); \
} \
}; \
} \
JPH_SUPPRESS_WARNING_POP
#define JPH_MAKE_HASHABLE(type, ...) \
JPH_SUPPRESS_WARNING_PUSH \
JPH_SUPPRESS_WARNINGS \
namespace JPH \
{ \
template<> \
JPH_MAKE_HASH_STRUCT(type, Hash<type>, __VA_ARGS__) \
} \
JPH_SUPPRESS_WARNING_POP \
JPH_MAKE_STD_HASH(type)
JPH_NAMESPACE_END

View file

@ -0,0 +1,876 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <Jolt/Math/BVec16.h>
JPH_NAMESPACE_BEGIN
/// Helper class for implementing an UnorderedSet or UnorderedMap
/// Based on CppCon 2017: Matt Kulukundis "Designing a Fast, Efficient, Cache-friendly Hash Table, Step by Step"
/// See: https://www.youtube.com/watch?v=ncHmEUmJZf4
template <class Key, class KeyValue, class HashTableDetail, class Hash, class KeyEqual>
class HashTable
{
public:
/// Properties
using value_type = KeyValue;
using size_type = uint32;
using difference_type = ptrdiff_t;
private:
/// Base class for iterators
template <class Table, class Iterator>
class IteratorBase
{
public:
/// Properties
using difference_type = typename Table::difference_type;
using value_type = typename Table::value_type;
using iterator_category = std::forward_iterator_tag;
/// Copy constructor
IteratorBase(const IteratorBase &inRHS) = default;
/// Assignment operator
IteratorBase & operator = (const IteratorBase &inRHS) = default;
/// Iterator at start of table
explicit IteratorBase(Table *inTable) :
mTable(inTable),
mIndex(0)
{
while (mIndex < mTable->mMaxSize && (mTable->mControl[mIndex] & cBucketUsed) == 0)
++mIndex;
}
/// Iterator at specific index
IteratorBase(Table *inTable, size_type inIndex) :
mTable(inTable),
mIndex(inIndex)
{
}
/// Prefix increment
Iterator & operator ++ ()
{
JPH_ASSERT(IsValid());
do
{
++mIndex;
}
while (mIndex < mTable->mMaxSize && (mTable->mControl[mIndex] & cBucketUsed) == 0);
return static_cast<Iterator &>(*this);
}
/// Postfix increment
Iterator operator ++ (int)
{
Iterator result(mTable, mIndex);
++(*this);
return result;
}
/// Access to key value pair
const KeyValue & operator * () const
{
JPH_ASSERT(IsValid());
return mTable->mData[mIndex];
}
/// Access to key value pair
const KeyValue * operator -> () const
{
JPH_ASSERT(IsValid());
return mTable->mData + mIndex;
}
/// Equality operator
bool operator == (const Iterator &inRHS) const
{
return mIndex == inRHS.mIndex && mTable == inRHS.mTable;
}
/// Inequality operator
bool operator != (const Iterator &inRHS) const
{
return !(*this == inRHS);
}
/// Check that the iterator is valid
bool IsValid() const
{
return mIndex < mTable->mMaxSize
&& (mTable->mControl[mIndex] & cBucketUsed) != 0;
}
Table * mTable;
size_type mIndex;
};
/// Get the maximum number of elements that we can support given a number of buckets
static constexpr size_type sGetMaxLoad(size_type inBucketCount)
{
return uint32((cMaxLoadFactorNumerator * inBucketCount) / cMaxLoadFactorDenominator);
}
/// Update the control value for a bucket
JPH_INLINE void SetControlValue(size_type inIndex, uint8 inValue)
{
JPH_ASSERT(inIndex < mMaxSize);
mControl[inIndex] = inValue;
// Mirror the first 15 bytes to the 15 bytes beyond mMaxSize
// Note that this is equivalent to:
// if (inIndex < 15)
// mControl[inIndex + mMaxSize] = inValue
// else
// mControl[inIndex] = inValue
// Which performs a needless write if inIndex >= 15 but at least it is branch-less
mControl[((inIndex - 15) & (mMaxSize - 1)) + 15] = inValue;
}
/// Get the index and control value for a particular key
JPH_INLINE void GetIndexAndControlValue(const Key &inKey, size_type &outIndex, uint8 &outControl) const
{
// Calculate hash
uint64 hash_value = Hash { } (inKey);
// Split hash into index and control value
outIndex = size_type(hash_value >> 7) & (mMaxSize - 1);
outControl = cBucketUsed | uint8(hash_value);
}
/// Allocate space for the hash table
void AllocateTable(size_type inMaxSize)
{
JPH_ASSERT(mData == nullptr);
mMaxSize = inMaxSize;
mLoadLeft = sGetMaxLoad(inMaxSize);
size_t required_size = size_t(mMaxSize) * (sizeof(KeyValue) + 1) + 15; // Add 15 bytes to mirror the first 15 bytes of the control values
if constexpr (cNeedsAlignedAllocate)
mData = reinterpret_cast<KeyValue *>(AlignedAllocate(required_size, alignof(KeyValue)));
else
mData = reinterpret_cast<KeyValue *>(Allocate(required_size));
mControl = reinterpret_cast<uint8 *>(mData + mMaxSize);
}
/// Copy the contents of another hash table
void CopyTable(const HashTable &inRHS)
{
if (inRHS.empty())
return;
AllocateTable(inRHS.mMaxSize);
// Copy control bytes
memcpy(mControl, inRHS.mControl, mMaxSize + 15);
// Copy elements
uint index = 0;
for (const uint8 *control = mControl, *control_end = mControl + mMaxSize; control != control_end; ++control, ++index)
if (*control & cBucketUsed)
new (mData + index) KeyValue(inRHS.mData[index]);
mSize = inRHS.mSize;
}
/// Grow the table to a new size
void GrowTable(size_type inNewMaxSize)
{
// Move the old table to a temporary structure
size_type old_max_size = mMaxSize;
KeyValue *old_data = mData;
const uint8 *old_control = mControl;
mData = nullptr;
mControl = nullptr;
mSize = 0;
mMaxSize = 0;
mLoadLeft = 0;
// Allocate new table
AllocateTable(inNewMaxSize);
// Reset all control bytes
memset(mControl, cBucketEmpty, mMaxSize + 15);
if (old_data != nullptr)
{
// Copy all elements from the old table
for (size_type i = 0; i < old_max_size; ++i)
if (old_control[i] & cBucketUsed)
{
size_type index;
KeyValue *element = old_data + i;
JPH_IF_ENABLE_ASSERTS(bool inserted =) InsertKey</* InsertAfterGrow= */ true>(HashTableDetail::sGetKey(*element), index);
JPH_ASSERT(inserted);
new (mData + index) KeyValue(std::move(*element));
element->~KeyValue();
}
// Free memory
if constexpr (cNeedsAlignedAllocate)
AlignedFree(old_data);
else
Free(old_data);
}
}
protected:
/// Get an element by index
KeyValue & GetElement(size_type inIndex) const
{
return mData[inIndex];
}
/// Insert a key into the map, returns true if the element was inserted, false if it already existed.
/// outIndex is the index at which the element should be constructed / where it is located.
template <bool InsertAfterGrow = false>
bool InsertKey(const Key &inKey, size_type &outIndex)
{
// Ensure we have enough space
if (mLoadLeft == 0)
{
// Should not be growing if we're already growing!
if constexpr (InsertAfterGrow)
JPH_ASSERT(false);
// Decide if we need to clean up all tombstones or if we need to grow the map
size_type num_deleted = sGetMaxLoad(mMaxSize) - mSize;
if (num_deleted * cMaxDeletedElementsDenominator > mMaxSize * cMaxDeletedElementsNumerator)
rehash(0);
else
{
// Grow by a power of 2
size_type new_max_size = max<size_type>(mMaxSize << 1, 16);
if (new_max_size < mMaxSize)
{
JPH_ASSERT(false, "Overflow in hash table size, can't grow!");
return false;
}
GrowTable(new_max_size);
}
}
// Split hash into index and control value
size_type index;
uint8 control;
GetIndexAndControlValue(inKey, index, control);
// Keeps track of the index of the first deleted bucket we found
constexpr size_type cNoDeleted = ~size_type(0);
size_type first_deleted_index = cNoDeleted;
// Linear probing
KeyEqual equal;
size_type bucket_mask = mMaxSize - 1;
BVec16 control16 = BVec16::sReplicate(control);
BVec16 bucket_empty = BVec16::sZero();
BVec16 bucket_deleted = BVec16::sReplicate(cBucketDeleted);
for (;;)
{
// Read 16 control values (note that we added 15 bytes at the end of the control values that mirror the first 15 bytes)
BVec16 control_bytes = BVec16::sLoadByte16(mControl + index);
// Check if we must find the element before we can insert
if constexpr (!InsertAfterGrow)
{
// Check for the control value we're looking for
// Note that when deleting we can create empty buckets instead of deleted buckets.
// This means we must unconditionally check all buckets in this batch for equality
// (also beyond the first empty bucket).
uint32 control_equal = uint32(BVec16::sEquals(control_bytes, control16).GetTrues());
// Index within the 16 buckets
size_type local_index = index;
// Loop while there's still buckets to process
while (control_equal != 0)
{
// Get the first equal bucket
uint first_equal = CountTrailingZeros(control_equal);
// Skip to the bucket
local_index += first_equal;
// Make sure that our index is not beyond the end of the table
local_index &= bucket_mask;
// We found a bucket with same control value
if (equal(HashTableDetail::sGetKey(mData[local_index]), inKey))
{
// Element already exists
outIndex = local_index;
return false;
}
// Skip past this bucket
control_equal >>= first_equal + 1;
local_index++;
}
// Check if we're still scanning for deleted buckets
if (first_deleted_index == cNoDeleted)
{
// Check if any buckets have been deleted, if so store the first one
uint32 control_deleted = uint32(BVec16::sEquals(control_bytes, bucket_deleted).GetTrues());
if (control_deleted != 0)
first_deleted_index = index + CountTrailingZeros(control_deleted);
}
}
// Check for empty buckets
uint32 control_empty = uint32(BVec16::sEquals(control_bytes, bucket_empty).GetTrues());
if (control_empty != 0)
{
// If we found a deleted bucket, use it.
// It doesn't matter if it is before or after the first empty bucket we found
// since we will always be scanning in batches of 16 buckets.
if (first_deleted_index == cNoDeleted || InsertAfterGrow)
{
index += CountTrailingZeros(control_empty);
--mLoadLeft; // Using an empty bucket decreases the load left
}
else
{
index = first_deleted_index;
}
// Make sure that our index is not beyond the end of the table
index &= bucket_mask;
// Update control byte
SetControlValue(index, control);
++mSize;
// Return index to newly allocated bucket
outIndex = index;
return true;
}
// Move to next batch of 16 buckets
index = (index + 16) & bucket_mask;
}
}
public:
/// Non-const iterator
class iterator : public IteratorBase<HashTable, iterator>
{
using Base = IteratorBase<HashTable, iterator>;
public:
using IteratorBase<HashTable, iterator>::operator ==;
/// Properties
using reference = typename Base::value_type &;
using pointer = typename Base::value_type *;
/// Constructors
explicit iterator(HashTable *inTable) : Base(inTable) { }
iterator(HashTable *inTable, size_type inIndex) : Base(inTable, inIndex) { }
iterator(const iterator &inIterator) : Base(inIterator) { }
/// Assignment
iterator & operator = (const iterator &inRHS) { Base::operator = (inRHS); return *this; }
using Base::operator *;
/// Non-const access to key value pair
KeyValue & operator * ()
{
JPH_ASSERT(this->IsValid());
return this->mTable->mData[this->mIndex];
}
using Base::operator ->;
/// Non-const access to key value pair
KeyValue * operator -> ()
{
JPH_ASSERT(this->IsValid());
return this->mTable->mData + this->mIndex;
}
};
/// Const iterator
class const_iterator : public IteratorBase<const HashTable, const_iterator>
{
using Base = IteratorBase<const HashTable, const_iterator>;
public:
using IteratorBase<const HashTable, const_iterator>::operator ==;
/// Properties
using reference = const typename Base::value_type &;
using pointer = const typename Base::value_type *;
/// Constructors
explicit const_iterator(const HashTable *inTable) : Base(inTable) { }
const_iterator(const HashTable *inTable, size_type inIndex) : Base(inTable, inIndex) { }
const_iterator(const const_iterator &inRHS) : Base(inRHS) { }
const_iterator(const iterator &inIterator) : Base(inIterator.mTable, inIterator.mIndex) { }
/// Assignment
const_iterator & operator = (const iterator &inRHS) { this->mTable = inRHS.mTable; this->mIndex = inRHS.mIndex; return *this; }
const_iterator & operator = (const const_iterator &inRHS) { Base::operator = (inRHS); return *this; }
};
/// Default constructor
HashTable() = default;
/// Copy constructor
HashTable(const HashTable &inRHS)
{
CopyTable(inRHS);
}
/// Move constructor
HashTable(HashTable &&ioRHS) noexcept :
mData(ioRHS.mData),
mControl(ioRHS.mControl),
mSize(ioRHS.mSize),
mMaxSize(ioRHS.mMaxSize),
mLoadLeft(ioRHS.mLoadLeft)
{
ioRHS.mData = nullptr;
ioRHS.mControl = nullptr;
ioRHS.mSize = 0;
ioRHS.mMaxSize = 0;
ioRHS.mLoadLeft = 0;
}
/// Assignment operator
HashTable & operator = (const HashTable &inRHS)
{
if (this != &inRHS)
{
clear();
CopyTable(inRHS);
}
return *this;
}
/// Move assignment operator
HashTable & operator = (HashTable &&ioRHS) noexcept
{
if (this != &ioRHS)
{
clear();
mData = ioRHS.mData;
mControl = ioRHS.mControl;
mSize = ioRHS.mSize;
mMaxSize = ioRHS.mMaxSize;
mLoadLeft = ioRHS.mLoadLeft;
ioRHS.mData = nullptr;
ioRHS.mControl = nullptr;
ioRHS.mSize = 0;
ioRHS.mMaxSize = 0;
ioRHS.mLoadLeft = 0;
}
return *this;
}
/// Destructor
~HashTable()
{
clear();
}
/// Reserve memory for a certain number of elements
void reserve(size_type inMaxSize)
{
// Calculate max size based on load factor
size_type max_size = GetNextPowerOf2(max<uint32>((cMaxLoadFactorDenominator * inMaxSize) / cMaxLoadFactorNumerator, 16));
if (max_size <= mMaxSize)
return;
GrowTable(max_size);
}
/// Destroy the entire hash table
void clear()
{
// Delete all elements
if constexpr (!std::is_trivially_destructible<KeyValue>())
if (!empty())
for (size_type i = 0; i < mMaxSize; ++i)
if (mControl[i] & cBucketUsed)
mData[i].~KeyValue();
if (mData != nullptr)
{
// Free memory
if constexpr (cNeedsAlignedAllocate)
AlignedFree(mData);
else
Free(mData);
// Reset members
mData = nullptr;
mControl = nullptr;
mSize = 0;
mMaxSize = 0;
mLoadLeft = 0;
}
}
/// Destroy the entire hash table but keeps the memory allocated
void ClearAndKeepMemory()
{
// Destruct elements
if constexpr (!std::is_trivially_destructible<KeyValue>())
if (!empty())
for (size_type i = 0; i < mMaxSize; ++i)
if (mControl[i] & cBucketUsed)
mData[i].~KeyValue();
mSize = 0;
// If there are elements that are not marked cBucketEmpty, we reset them
size_type max_load = sGetMaxLoad(mMaxSize);
if (mLoadLeft != max_load)
{
// Reset all control bytes
memset(mControl, cBucketEmpty, mMaxSize + 15);
mLoadLeft = max_load;
}
}
/// Iterator to first element
iterator begin()
{
return iterator(this);
}
/// Iterator to one beyond last element
iterator end()
{
return iterator(this, mMaxSize);
}
/// Iterator to first element
const_iterator begin() const
{
return const_iterator(this);
}
/// Iterator to one beyond last element
const_iterator end() const
{
return const_iterator(this, mMaxSize);
}
/// Iterator to first element
const_iterator cbegin() const
{
return const_iterator(this);
}
/// Iterator to one beyond last element
const_iterator cend() const
{
return const_iterator(this, mMaxSize);
}
/// Number of buckets in the table
size_type bucket_count() const
{
return mMaxSize;
}
/// Max number of buckets that the table can have
constexpr size_type max_bucket_count() const
{
return size_type(1) << (sizeof(size_type) * 8 - 1);
}
/// Check if there are no elements in the table
bool empty() const
{
return mSize == 0;
}
/// Number of elements in the table
size_type size() const
{
return mSize;
}
/// Max number of elements that the table can hold
constexpr size_type max_size() const
{
return size_type((uint64(max_bucket_count()) * cMaxLoadFactorNumerator) / cMaxLoadFactorDenominator);
}
/// Get the max load factor for this table (max number of elements / number of buckets)
constexpr float max_load_factor() const
{
return float(cMaxLoadFactorNumerator) / float(cMaxLoadFactorDenominator);
}
/// Insert a new element, returns iterator and if the element was inserted
std::pair<iterator, bool> insert(const value_type &inValue)
{
size_type index;
bool inserted = InsertKey(HashTableDetail::sGetKey(inValue), index);
if (inserted)
new (mData + index) KeyValue(inValue);
return std::make_pair(iterator(this, index), inserted);
}
/// Find an element, returns iterator to element or end() if not found
const_iterator find(const Key &inKey) const
{
// Check if we have any data
if (empty())
return cend();
// Split hash into index and control value
size_type index;
uint8 control;
GetIndexAndControlValue(inKey, index, control);
// Linear probing
KeyEqual equal;
size_type bucket_mask = mMaxSize - 1;
BVec16 control16 = BVec16::sReplicate(control);
BVec16 bucket_empty = BVec16::sZero();
for (;;)
{
// Read 16 control values
// (note that we added 15 bytes at the end of the control values that mirror the first 15 bytes)
BVec16 control_bytes = BVec16::sLoadByte16(mControl + index);
// Check for the control value we're looking for
// Note that when deleting we can create empty buckets instead of deleted buckets.
// This means we must unconditionally check all buckets in this batch for equality
// (also beyond the first empty bucket).
uint32 control_equal = uint32(BVec16::sEquals(control_bytes, control16).GetTrues());
// Index within the 16 buckets
size_type local_index = index;
// Loop while there's still buckets to process
while (control_equal != 0)
{
// Get the first equal bucket
uint first_equal = CountTrailingZeros(control_equal);
// Skip to the bucket
local_index += first_equal;
// Make sure that our index is not beyond the end of the table
local_index &= bucket_mask;
// We found a bucket with same control value
if (equal(HashTableDetail::sGetKey(mData[local_index]), inKey))
{
// Element found
return const_iterator(this, local_index);
}
// Skip past this bucket
control_equal >>= first_equal + 1;
local_index++;
}
// Check for empty buckets
uint32 control_empty = uint32(BVec16::sEquals(control_bytes, bucket_empty).GetTrues());
if (control_empty != 0)
{
// An empty bucket was found, we didn't find the element
return cend();
}
// Move to next batch of 16 buckets
index = (index + 16) & bucket_mask;
}
}
/// @brief Erase an element by iterator
void erase(const const_iterator &inIterator)
{
JPH_ASSERT(inIterator.IsValid());
// Read 16 control values before and after the current index
// (note that we added 15 bytes at the end of the control values that mirror the first 15 bytes)
BVec16 control_bytes_before = BVec16::sLoadByte16(mControl + ((inIterator.mIndex - 16) & (mMaxSize - 1)));
BVec16 control_bytes_after = BVec16::sLoadByte16(mControl + inIterator.mIndex);
BVec16 bucket_empty = BVec16::sZero();
uint32 control_empty_before = uint32(BVec16::sEquals(control_bytes_before, bucket_empty).GetTrues());
uint32 control_empty_after = uint32(BVec16::sEquals(control_bytes_after, bucket_empty).GetTrues());
// If (this index including) there exist 16 consecutive non-empty slots (represented by a bit being 0) then
// a probe looking for some element needs to continue probing so we cannot mark the bucket as empty
// but must mark it as deleted instead.
// Note that we use: CountLeadingZeros(uint16) = CountLeadingZeros(uint32) - 16.
uint8 control_value = CountLeadingZeros(control_empty_before) - 16 + CountTrailingZeros(control_empty_after) < 16? cBucketEmpty : cBucketDeleted;
// Mark the bucket as empty/deleted
SetControlValue(inIterator.mIndex, control_value);
// Destruct the element
mData[inIterator.mIndex].~KeyValue();
// If we marked the bucket as empty we can increase the load left
if (control_value == cBucketEmpty)
++mLoadLeft;
// Decrease size
--mSize;
}
/// @brief Erase an element by key
size_type erase(const Key &inKey)
{
const_iterator it = find(inKey);
if (it == cend())
return 0;
erase(it);
return 1;
}
/// Swap the contents of two hash tables
void swap(HashTable &ioRHS) noexcept
{
std::swap(mData, ioRHS.mData);
std::swap(mControl, ioRHS.mControl);
std::swap(mSize, ioRHS.mSize);
std::swap(mMaxSize, ioRHS.mMaxSize);
std::swap(mLoadLeft, ioRHS.mLoadLeft);
}
/// In place re-hashing of all elements in the table. Removes all cBucketDeleted elements
/// The std version takes a bucket count, but we just re-hash to the same size.
void rehash(size_type)
{
// Update the control value for all buckets
for (size_type i = 0; i < mMaxSize; ++i)
{
uint8 &control = mControl[i];
switch (control)
{
case cBucketDeleted:
// Deleted buckets become empty
control = cBucketEmpty;
break;
case cBucketEmpty:
// Remains empty
break;
default:
// Mark all occupied as deleted, to indicate it needs to move to the correct place
control = cBucketDeleted;
break;
}
}
// Replicate control values to the last 15 entries
for (size_type i = 0; i < 15; ++i)
mControl[mMaxSize + i] = mControl[i];
// Loop over all elements that have been 'deleted' and move them to their new spot
BVec16 bucket_used = BVec16::sReplicate(cBucketUsed);
size_type bucket_mask = mMaxSize - 1;
uint32 probe_mask = bucket_mask & ~uint32(0b1111); // Mask out lower 4 bits because we test 16 buckets at a time
for (size_type src = 0; src < mMaxSize; ++src)
if (mControl[src] == cBucketDeleted)
for (;;)
{
// Split hash into index and control value
size_type src_index;
uint8 src_control;
GetIndexAndControlValue(HashTableDetail::sGetKey(mData[src]), src_index, src_control);
// Linear probing
size_type dst = src_index;
for (;;)
{
// Check if any buckets are free
BVec16 control_bytes = BVec16::sLoadByte16(mControl + dst);
uint32 control_free = uint32(BVec16::sAnd(control_bytes, bucket_used).GetTrues()) ^ 0xffff;
if (control_free != 0)
{
// Select this bucket as destination
dst += CountTrailingZeros(control_free);
dst &= bucket_mask;
break;
}
// Move to next batch of 16 buckets
dst = (dst + 16) & bucket_mask;
}
// Check if we stay in the same probe group
if (((dst - src_index) & probe_mask) == ((src - src_index) & probe_mask))
{
// We stay in the same group, we can stay where we are
SetControlValue(src, src_control);
break;
}
else if (mControl[dst] == cBucketEmpty)
{
// There's an empty bucket, move us there
SetControlValue(dst, src_control);
SetControlValue(src, cBucketEmpty);
new (mData + dst) KeyValue(std::move(mData[src]));
mData[src].~KeyValue();
break;
}
else
{
// There's an element in the bucket we want to move to, swap them
JPH_ASSERT(mControl[dst] == cBucketDeleted);
SetControlValue(dst, src_control);
std::swap(mData[src], mData[dst]);
// Iterate again with the same source bucket
}
}
// Reinitialize load left
mLoadLeft = sGetMaxLoad(mMaxSize) - mSize;
}
private:
/// If this allocator needs to fall back to aligned allocations because the type requires it
static constexpr bool cNeedsAlignedAllocate = alignof(KeyValue) > JPH_DEFAULT_ALLOCATE_ALIGNMENT;
/// Max load factor is cMaxLoadFactorNumerator / cMaxLoadFactorDenominator
static constexpr uint64 cMaxLoadFactorNumerator = 7;
static constexpr uint64 cMaxLoadFactorDenominator = 8;
/// If we can recover this fraction of deleted elements, we'll reshuffle the buckets in place rather than growing the table
static constexpr uint64 cMaxDeletedElementsNumerator = 1;
static constexpr uint64 cMaxDeletedElementsDenominator = 8;
/// Values that the control bytes can have
static constexpr uint8 cBucketEmpty = 0;
static constexpr uint8 cBucketDeleted = 0x7f;
static constexpr uint8 cBucketUsed = 0x80; // Lowest 7 bits are lowest 7 bits of the hash value
/// The buckets, an array of size mMaxSize
KeyValue * mData = nullptr;
/// Control bytes, an array of size mMaxSize + 15
uint8 * mControl = nullptr;
/// Number of elements in the table
size_type mSize = 0;
/// Max number of elements that can be stored in the table
size_type mMaxSize = 0;
/// Number of elements we can add to the table before we need to grow
size_type mLoadLeft = 0;
};
JPH_NAMESPACE_END

View file

@ -0,0 +1,36 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2025 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#ifdef JPH_PLATFORM_WINDOWS
JPH_SUPPRESS_WARNING_PUSH
JPH_MSVC_SUPPRESS_WARNING(5039) // winbase.h(13179): warning C5039: 'TpSetCallbackCleanupGroup': pointer or reference to potentially throwing function passed to 'extern "C"' function under -EHc. Undefined behavior may occur if this function throws an exception.
JPH_MSVC2026_PLUS_SUPPRESS_WARNING(4865) // wingdi.h(2806,1): '<unnamed-enum-DISPLAYCONFIG_OUTPUT_TECHNOLOGY_OTHER>': the underlying type will change from 'int' to '__int64' when '/Zc:enumTypes' is specified on the command line
JPH_CLANG_SUPPRESS_WARNING("-Wreserved-macro-identifier") // Complains about _WIN32_WINNT being reserved
#ifndef WINVER
#define WINVER 0x0A00 // Targeting Windows 10 and above
#endif
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0A00
#endif
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef JPH_COMPILER_MINGW
#include <Windows.h>
#else
#include <windows.h>
#endif
JPH_SUPPRESS_WARNING_POP
#endif

Some files were not shown because too many files have changed in this diff Show more